Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/thirdparty/jolt_physics/Jolt/Core/Profiler.cpp
9906 views
1
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
2
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
3
// SPDX-License-Identifier: MIT
4
5
#include <Jolt/Jolt.h>
6
7
#include <Jolt/Core/Profiler.h>
8
#include <Jolt/Core/Color.h>
9
#include <Jolt/Core/StringTools.h>
10
#include <Jolt/Core/QuickSort.h>
11
12
JPH_SUPPRESS_WARNINGS_STD_BEGIN
13
#include <fstream>
14
JPH_SUPPRESS_WARNINGS_STD_END
15
16
JPH_NAMESPACE_BEGIN
17
18
#if defined(JPH_EXTERNAL_PROFILE) && defined(JPH_SHARED_LIBRARY)
19
20
ProfileStartMeasurementFunction ProfileStartMeasurement = [](const char *, uint32, uint8 *) { };
21
ProfileEndMeasurementFunction ProfileEndMeasurement = [](uint8 *) { };
22
23
#elif defined(JPH_PROFILE_ENABLED)
24
25
//////////////////////////////////////////////////////////////////////////////////////////
26
// Profiler
27
//////////////////////////////////////////////////////////////////////////////////////////
28
29
Profiler *Profiler::sInstance = nullptr;
30
31
#ifdef JPH_SHARED_LIBRARY
32
static thread_local ProfileThread *sInstance = nullptr;
33
34
ProfileThread *ProfileThread::sGetInstance()
35
{
36
return sInstance;
37
}
38
39
void ProfileThread::sSetInstance(ProfileThread *inInstance)
40
{
41
sInstance = inInstance;
42
}
43
#else
44
thread_local ProfileThread *ProfileThread::sInstance = nullptr;
45
#endif
46
47
bool ProfileMeasurement::sOutOfSamplesReported = false;
48
49
void Profiler::UpdateReferenceTime()
50
{
51
mReferenceTick = GetProcessorTickCount();
52
mReferenceTime = std::chrono::high_resolution_clock::now();
53
}
54
55
uint64 Profiler::GetProcessorTicksPerSecond() const
56
{
57
uint64 ticks = GetProcessorTickCount();
58
std::chrono::high_resolution_clock::time_point time = std::chrono::high_resolution_clock::now();
59
60
return (ticks - mReferenceTick) * 1000000000ULL / std::chrono::duration_cast<std::chrono::nanoseconds>(time - mReferenceTime).count();
61
}
62
63
// This function assumes that none of the threads are active while we're dumping the profile,
64
// otherwise there will be a race condition on mCurrentSample and the profile data.
65
JPH_TSAN_NO_SANITIZE
66
void Profiler::NextFrame()
67
{
68
std::lock_guard lock(mLock);
69
70
if (mDump)
71
{
72
DumpInternal();
73
mDump = false;
74
}
75
76
for (ProfileThread *t : mThreads)
77
t->mCurrentSample = 0;
78
79
UpdateReferenceTime();
80
}
81
82
void Profiler::Dump(const string_view &inTag)
83
{
84
mDump = true;
85
mDumpTag = inTag;
86
}
87
88
void Profiler::AddThread(ProfileThread *inThread)
89
{
90
std::lock_guard lock(mLock);
91
92
mThreads.push_back(inThread);
93
}
94
95
void Profiler::RemoveThread(ProfileThread *inThread)
96
{
97
std::lock_guard lock(mLock);
98
99
Array<ProfileThread *>::iterator i = std::find(mThreads.begin(), mThreads.end(), inThread);
100
JPH_ASSERT(i != mThreads.end());
101
mThreads.erase(i);
102
}
103
104
void Profiler::sAggregate(int inDepth, uint32 inColor, ProfileSample *&ioSample, const ProfileSample *inEnd, Aggregators &ioAggregators, KeyToAggregator &ioKeyToAggregator)
105
{
106
// Store depth
107
ioSample->mDepth = uint8(min(255, inDepth));
108
109
// Update color
110
if (ioSample->mColor == 0)
111
ioSample->mColor = inColor;
112
else
113
inColor = ioSample->mColor;
114
115
// Start accumulating totals
116
uint64 cycles_this_with_children = ioSample->mEndCycle - ioSample->mStartCycle;
117
118
// Loop over following samples until we find a sample that starts on or after our end
119
ProfileSample *sample;
120
for (sample = ioSample + 1; sample < inEnd && sample->mStartCycle < ioSample->mEndCycle; ++sample)
121
{
122
JPH_ASSERT(sample[-1].mStartCycle <= sample->mStartCycle);
123
JPH_ASSERT(sample->mStartCycle >= ioSample->mStartCycle);
124
JPH_ASSERT(sample->mEndCycle <= ioSample->mEndCycle);
125
126
// Recurse and skip over the children of this child
127
sAggregate(inDepth + 1, inColor, sample, inEnd, ioAggregators, ioKeyToAggregator);
128
}
129
130
// Find the aggregator for this name / filename pair
131
Aggregator *aggregator;
132
KeyToAggregator::iterator aggregator_idx = ioKeyToAggregator.find(ioSample->mName);
133
if (aggregator_idx == ioKeyToAggregator.end())
134
{
135
// Not found, add to map and insert in array
136
ioKeyToAggregator.try_emplace(ioSample->mName, ioAggregators.size());
137
ioAggregators.emplace_back(ioSample->mName);
138
aggregator = &ioAggregators.back();
139
}
140
else
141
{
142
// Found
143
aggregator = &ioAggregators[aggregator_idx->second];
144
}
145
146
// Add the measurement to the aggregator
147
aggregator->AccumulateMeasurement(cycles_this_with_children);
148
149
// Update ioSample to the last child of ioSample
150
JPH_ASSERT(sample[-1].mStartCycle <= ioSample->mEndCycle);
151
JPH_ASSERT(sample >= inEnd || sample->mStartCycle >= ioSample->mEndCycle);
152
ioSample = sample - 1;
153
}
154
155
void Profiler::DumpInternal()
156
{
157
// Freeze data from threads
158
// Note that this is not completely thread safe: As a profile sample is added mCurrentSample is incremented
159
// but the data is not written until the sample finishes. So if we dump the profile information while
160
// some other thread is running, we may get some garbage information from the previous frame
161
Threads threads;
162
for (ProfileThread *t : mThreads)
163
threads.push_back({ t->mThreadName, t->mSamples, t->mSamples + t->mCurrentSample });
164
165
// Shift all samples so that the first sample is at zero
166
uint64 min_cycle = 0xffffffffffffffffUL;
167
for (const ThreadSamples &t : threads)
168
if (t.mSamplesBegin < t.mSamplesEnd)
169
min_cycle = min(min_cycle, t.mSamplesBegin[0].mStartCycle);
170
for (const ThreadSamples &t : threads)
171
for (ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)
172
{
173
s->mStartCycle -= min_cycle;
174
s->mEndCycle -= min_cycle;
175
}
176
177
// Determine tag of this profile
178
String tag;
179
if (mDumpTag.empty())
180
{
181
// Next sequence number
182
static int number = 0;
183
++number;
184
tag = ConvertToString(number);
185
}
186
else
187
{
188
// Take provided tag
189
tag = mDumpTag;
190
mDumpTag.clear();
191
}
192
193
// Aggregate data across threads
194
Aggregators aggregators;
195
KeyToAggregator key_to_aggregators;
196
for (const ThreadSamples &t : threads)
197
for (ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)
198
sAggregate(0, Color::sGetDistinctColor(0).GetUInt32(), s, end, aggregators, key_to_aggregators);
199
200
// Dump as chart
201
DumpChart(tag.c_str(), threads, key_to_aggregators, aggregators);
202
}
203
204
static String sHTMLEncode(const char *inString)
205
{
206
String str(inString);
207
StringReplace(str, "<", "&lt;");
208
StringReplace(str, ">", "&gt;");
209
return str;
210
}
211
212
void Profiler::DumpChart(const char *inTag, const Threads &inThreads, const KeyToAggregator &inKeyToAggregators, const Aggregators &inAggregators)
213
{
214
// Open file
215
std::ofstream f;
216
f.open(StringFormat("profile_chart_%s.html", inTag).c_str(), std::ofstream::out | std::ofstream::trunc);
217
if (!f.is_open())
218
return;
219
220
// Write header
221
f << R"(<!DOCTYPE html>
222
<html>
223
<head>
224
<title>Profile Chart</title>
225
<style>
226
html, body {
227
padding: 0px;
228
border: 0px;
229
margin: 0px;
230
width: 100%;
231
height: 100%;
232
overflow: hidden;
233
}
234
235
canvas {
236
position: absolute;
237
top: 10px;
238
left: 10px;
239
padding: 0px;
240
border: 0px;
241
margin: 0px;
242
}
243
244
#tooltip {
245
font: Courier New;
246
position: absolute;
247
background-color: white;
248
border: 1px;
249
border-style: solid;
250
border-color: black;
251
pointer-events: none;
252
padding: 5px;
253
font: 14px Arial;
254
visibility: hidden;
255
height: auto;
256
}
257
258
.stat {
259
color: blue;
260
text-align: right;
261
}
262
</style>
263
<script type="text/javascript">
264
var canvas;
265
var ctx;
266
var tooltip;
267
var min_scale;
268
var scale;
269
var offset_x = 0;
270
var offset_y = 0;
271
var size_y;
272
var dragging = false;
273
var previous_x = 0;
274
var previous_y = 0;
275
var bar_height = 15;
276
var line_height = bar_height + 2;
277
var thread_separation = 6;
278
var thread_font_size = 12;
279
var thread_font = thread_font_size + "px Arial";
280
var bar_font_size = 10;
281
var bar_font = bar_font_size + "px Arial";
282
var end_cycle = 0;
283
284
function drawChart()
285
{
286
ctx.clearRect(0, 0, canvas.width, canvas.height);
287
288
ctx.lineWidth = 1;
289
290
var y = offset_y;
291
292
for (var t = 0; t < threads.length; t++)
293
{
294
// Check if thread has samples
295
var thread = threads[t];
296
if (thread.start.length == 0)
297
continue;
298
299
// Draw thread name
300
y += thread_font_size;
301
ctx.font = thread_font;
302
ctx.fillStyle = "#000000";
303
ctx.fillText(thread.thread_name, 0, y);
304
y += thread_separation;
305
306
// Draw outlines for each bar of samples
307
ctx.fillStyle = "#c0c0c0";
308
for (var d = 0; d <= thread.max_depth; d++)
309
ctx.fillRect(0, y + d * line_height, canvas.width, bar_height);
310
311
// Draw samples
312
ctx.font = bar_font;
313
for (var s = 0; s < thread.start.length; s++)
314
{
315
// Cull bar
316
var rx = scale * (offset_x + thread.start[s]);
317
if (rx > canvas.width) // right of canvas
318
break;
319
var rw = scale * thread.cycles[s];
320
if (rw < 0.5) // less than half pixel, skip
321
continue;
322
if (rx + rw < 0) // left of canvas
323
continue;
324
325
// Draw bar
326
var ry = y + line_height * thread.depth[s];
327
ctx.fillStyle = thread.color[s];
328
ctx.fillRect(rx, ry, rw, bar_height);
329
ctx.strokeStyle = thread.darkened_color[s];
330
ctx.strokeRect(rx, ry, rw, bar_height);
331
332
// Get index in aggregated list
333
var a = thread.aggregator[s];
334
335
// Draw text
336
if (rw > aggregated.name_width[a])
337
{
338
ctx.fillStyle = "#000000";
339
ctx.fillText(aggregated.name[a], rx + (rw - aggregated.name_width[a]) / 2, ry + bar_height - 4);
340
}
341
}
342
343
// Next line
344
y += line_height * (1 + thread.max_depth) + thread_separation;
345
}
346
347
// Update size
348
size_y = y - offset_y;
349
}
350
351
function drawTooltip(mouse_x, mouse_y)
352
{
353
var y = offset_y;
354
355
for (var t = 0; t < threads.length; t++)
356
{
357
// Check if thread has samples
358
var thread = threads[t];
359
if (thread.start.length == 0)
360
continue;
361
362
// Thead name
363
y += thread_font_size + thread_separation;
364
365
// Draw samples
366
for (var s = 0; s < thread.start.length; s++)
367
{
368
// Cull bar
369
var rx = scale * (offset_x + thread.start[s]);
370
if (rx > mouse_x)
371
break;
372
var rw = scale * thread.cycles[s];
373
if (rx + rw < mouse_x)
374
continue;
375
376
var ry = y + line_height * thread.depth[s];
377
if (mouse_y >= ry && mouse_y < ry + bar_height)
378
{
379
// Get index into aggregated list
380
var a = thread.aggregator[s];
381
382
// Found bar, fill in tooltip
383
tooltip.style.left = (canvas.offsetLeft + mouse_x) + "px";
384
tooltip.style.top = (canvas.offsetTop + mouse_y) + "px";
385
tooltip.style.visibility = "visible";
386
tooltip.innerHTML = aggregated.name[a] + "<br>"
387
+ "<table>"
388
+ "<tr><td>Time:</td><td class=\"stat\">" + (1000000 * thread.cycles[s] / cycles_per_second).toFixed(2) + " &micro;s</td></tr>"
389
+ "<tr><td>Start:</td><td class=\"stat\">" + (1000000 * thread.start[s] / cycles_per_second).toFixed(2) + " &micro;s</td></tr>"
390
+ "<tr><td>End:</td><td class=\"stat\">" + (1000000 * (thread.start[s] + thread.cycles[s]) / cycles_per_second).toFixed(2) + " &micro;s</td></tr>"
391
+ "<tr><td>Avg. Time:</td><td class=\"stat\">" + (1000000 * aggregated.cycles_per_frame[a] / cycles_per_second / aggregated.calls[a]).toFixed(2) + " &micro;s</td></tr>"
392
+ "<tr><td>Min Time:</td><td class=\"stat\">" + (1000000 * aggregated.min_cycles[a] / cycles_per_second).toFixed(2) + " &micro;s</td></tr>"
393
+ "<tr><td>Max Time:</td><td class=\"stat\">" + (1000000 * aggregated.max_cycles[a] / cycles_per_second).toFixed(2) + " &micro;s</td></tr>"
394
+ "<tr><td>Time / Frame:</td><td class=\"stat\">" + (1000000 * aggregated.cycles_per_frame[a] / cycles_per_second).toFixed(2) + " &micro;s</td></tr>"
395
+ "<tr><td>Calls:</td><td class=\"stat\">" + aggregated.calls[a] + "</td></tr>"
396
+ "</table>";
397
return;
398
}
399
}
400
401
// Next line
402
y += line_height * (1 + thread.max_depth) + thread_separation;
403
}
404
405
// No bar found, hide tooltip
406
tooltip.style.visibility = "hidden";
407
}
408
409
function onMouseDown(evt)
410
{
411
dragging = true;
412
previous_x = evt.clientX, previous_y = evt.clientY;
413
tooltip.style.visibility = "hidden";
414
}
415
416
function onMouseUp(evt)
417
{
418
dragging = false;
419
}
420
421
function clampMotion()
422
{
423
// Clamp horizontally
424
var min_offset_x = canvas.width / scale - end_cycle;
425
if (offset_x < min_offset_x)
426
offset_x = min_offset_x;
427
if (offset_x > 0)
428
offset_x = 0;
429
430
// Clamp vertically
431
var min_offset_y = canvas.height - size_y;
432
if (offset_y < min_offset_y)
433
offset_y = min_offset_y;
434
if (offset_y > 0)
435
offset_y = 0;
436
437
// Clamp scale
438
if (scale < min_scale)
439
scale = min_scale;
440
var max_scale = 1000 * min_scale;
441
if (scale > max_scale)
442
scale = max_scale;
443
}
444
445
function onMouseMove(evt)
446
{
447
if (dragging)
448
{
449
// Calculate new offset
450
offset_x += (evt.clientX - previous_x) / scale;
451
offset_y += evt.clientY - previous_y;
452
453
clampMotion();
454
455
drawChart();
456
}
457
else
458
drawTooltip(evt.clientX - canvas.offsetLeft, evt.clientY - canvas.offsetTop);
459
460
previous_x = evt.clientX, previous_y = evt.clientY;
461
}
462
463
function onScroll(evt)
464
{
465
tooltip.style.visibility = "hidden";
466
467
var old_scale = scale;
468
if (evt.deltaY > 0)
469
scale /= 1.1;
470
else
471
scale *= 1.1;
472
473
clampMotion();
474
475
// Ensure that event under mouse stays under mouse
476
var x = previous_x - canvas.offsetLeft;
477
offset_x += x / scale - x / old_scale;
478
479
clampMotion();
480
481
drawChart();
482
}
483
484
function darkenColor(color)
485
{
486
var i = parseInt(color.slice(1), 16);
487
488
var r = i >> 16;
489
var g = (i >> 8) & 0xff;
490
var b = i & 0xff;
491
492
r = Math.round(0.8 * r);
493
g = Math.round(0.8 * g);
494
b = Math.round(0.8 * b);
495
496
i = (r << 16) + (g << 8) + b;
497
498
return "#" + i.toString(16);
499
}
500
501
function startChart()
502
{
503
// Fetch elements
504
canvas = document.getElementById('canvas');
505
ctx = canvas.getContext("2d");
506
tooltip = document.getElementById('tooltip');
507
508
// Resize canvas to fill screen
509
canvas.width = document.body.offsetWidth - 20;
510
canvas.height = document.body.offsetHeight - 20;
511
512
// Register mouse handlers
513
canvas.onmousedown = onMouseDown;
514
canvas.onmouseup = onMouseUp;
515
canvas.onmouseout = onMouseUp;
516
canvas.onmousemove = onMouseMove;
517
canvas.onwheel = onScroll;
518
519
for (var t = 0; t < threads.length; t++)
520
{
521
var thread = threads[t];
522
523
// Calculate darkened colors
524
thread.darkened_color = new Array(thread.color.length);
525
for (var s = 0; s < thread.color.length; s++)
526
thread.darkened_color[s] = darkenColor(thread.color[s]);
527
528
// Calculate max depth and end cycle
529
thread.max_depth = 0;
530
for (var s = 0; s < thread.start.length; s++)
531
{
532
thread.max_depth = Math.max(thread.max_depth, thread.depth[s]);
533
end_cycle = Math.max(end_cycle, thread.start[s] + thread.cycles[s]);
534
}
535
}
536
537
// Calculate width of name strings
538
ctx.font = bar_font;
539
aggregated.name_width = new Array(aggregated.name.length);
540
for (var a = 0; a < aggregated.name.length; a++)
541
aggregated.name_width[a] = ctx.measureText(aggregated.name[a]).width;
542
543
// Store scale properties
544
min_scale = canvas.width / end_cycle;
545
scale = min_scale;
546
547
drawChart();
548
}
549
</script>
550
</head>
551
<body onload="startChart();">
552
<script type="text/javascript">
553
)";
554
555
// Get cycles per second
556
uint64 cycles_per_second = GetProcessorTicksPerSecond();
557
f << "var cycles_per_second = " << cycles_per_second << ";\n";
558
559
// Dump samples
560
f << "var threads = [\n";
561
bool first_thread = true;
562
for (const ThreadSamples &t : inThreads)
563
{
564
if (!first_thread)
565
f << ",\n";
566
first_thread = false;
567
568
f << "{\nthread_name: \"" << t.mThreadName << "\",\naggregator: [";
569
bool first = true;
570
for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)
571
{
572
if (!first)
573
f << ",";
574
first = false;
575
f << inKeyToAggregators.find(s->mName)->second;
576
}
577
f << "],\ncolor: [";
578
first = true;
579
for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)
580
{
581
if (!first)
582
f << ",";
583
first = false;
584
Color c(s->mColor);
585
f << StringFormat("\"#%02x%02x%02x\"", c.r, c.g, c.b);
586
}
587
f << "],\nstart: [";
588
first = true;
589
for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)
590
{
591
if (!first)
592
f << ",";
593
first = false;
594
f << s->mStartCycle;
595
}
596
f << "],\ncycles: [";
597
first = true;
598
for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)
599
{
600
if (!first)
601
f << ",";
602
first = false;
603
f << s->mEndCycle - s->mStartCycle;
604
}
605
f << "],\ndepth: [";
606
first = true;
607
for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)
608
{
609
if (!first)
610
f << ",";
611
first = false;
612
f << int(s->mDepth);
613
}
614
f << "]\n}";
615
}
616
617
// Dump aggregated data
618
f << "];\nvar aggregated = {\nname: [";
619
bool first = true;
620
for (const Aggregator &a : inAggregators)
621
{
622
if (!first)
623
f << ",";
624
first = false;
625
String name = "\"" + sHTMLEncode(a.mName) + "\"";
626
f << name;
627
}
628
f << "],\ncalls: [";
629
first = true;
630
for (const Aggregator &a : inAggregators)
631
{
632
if (!first)
633
f << ",";
634
first = false;
635
f << a.mCallCounter;
636
}
637
f << "],\nmin_cycles: [";
638
first = true;
639
for (const Aggregator &a : inAggregators)
640
{
641
if (!first)
642
f << ",";
643
first = false;
644
f << a.mMinCyclesInCallWithChildren;
645
}
646
f << "],\nmax_cycles: [";
647
first = true;
648
for (const Aggregator &a : inAggregators)
649
{
650
if (!first)
651
f << ",";
652
first = false;
653
f << a.mMaxCyclesInCallWithChildren;
654
}
655
f << "],\ncycles_per_frame: [";
656
first = true;
657
for (const Aggregator &a : inAggregators)
658
{
659
if (!first)
660
f << ",";
661
first = false;
662
f << a.mTotalCyclesInCallWithChildren;
663
}
664
665
// Write footer
666
f << R"(]};
667
</script>
668
669
<canvas id="canvas"></canvas>
670
<div id="tooltip"></div>
671
672
</tbody></table></body></html>)";
673
}
674
675
#endif // JPH_PROFILE_ENABLED
676
677
JPH_NAMESPACE_END
678
679