Path: blob/master/thirdparty/jolt_physics/Jolt/Core/Profiler.cpp
9906 views
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)1// SPDX-FileCopyrightText: 2021 Jorrit Rouwe2// SPDX-License-Identifier: MIT34#include <Jolt/Jolt.h>56#include <Jolt/Core/Profiler.h>7#include <Jolt/Core/Color.h>8#include <Jolt/Core/StringTools.h>9#include <Jolt/Core/QuickSort.h>1011JPH_SUPPRESS_WARNINGS_STD_BEGIN12#include <fstream>13JPH_SUPPRESS_WARNINGS_STD_END1415JPH_NAMESPACE_BEGIN1617#if defined(JPH_EXTERNAL_PROFILE) && defined(JPH_SHARED_LIBRARY)1819ProfileStartMeasurementFunction ProfileStartMeasurement = [](const char *, uint32, uint8 *) { };20ProfileEndMeasurementFunction ProfileEndMeasurement = [](uint8 *) { };2122#elif defined(JPH_PROFILE_ENABLED)2324//////////////////////////////////////////////////////////////////////////////////////////25// Profiler26//////////////////////////////////////////////////////////////////////////////////////////2728Profiler *Profiler::sInstance = nullptr;2930#ifdef JPH_SHARED_LIBRARY31static thread_local ProfileThread *sInstance = nullptr;3233ProfileThread *ProfileThread::sGetInstance()34{35return sInstance;36}3738void ProfileThread::sSetInstance(ProfileThread *inInstance)39{40sInstance = inInstance;41}42#else43thread_local ProfileThread *ProfileThread::sInstance = nullptr;44#endif4546bool ProfileMeasurement::sOutOfSamplesReported = false;4748void Profiler::UpdateReferenceTime()49{50mReferenceTick = GetProcessorTickCount();51mReferenceTime = std::chrono::high_resolution_clock::now();52}5354uint64 Profiler::GetProcessorTicksPerSecond() const55{56uint64 ticks = GetProcessorTickCount();57std::chrono::high_resolution_clock::time_point time = std::chrono::high_resolution_clock::now();5859return (ticks - mReferenceTick) * 1000000000ULL / std::chrono::duration_cast<std::chrono::nanoseconds>(time - mReferenceTime).count();60}6162// This function assumes that none of the threads are active while we're dumping the profile,63// otherwise there will be a race condition on mCurrentSample and the profile data.64JPH_TSAN_NO_SANITIZE65void Profiler::NextFrame()66{67std::lock_guard lock(mLock);6869if (mDump)70{71DumpInternal();72mDump = false;73}7475for (ProfileThread *t : mThreads)76t->mCurrentSample = 0;7778UpdateReferenceTime();79}8081void Profiler::Dump(const string_view &inTag)82{83mDump = true;84mDumpTag = inTag;85}8687void Profiler::AddThread(ProfileThread *inThread)88{89std::lock_guard lock(mLock);9091mThreads.push_back(inThread);92}9394void Profiler::RemoveThread(ProfileThread *inThread)95{96std::lock_guard lock(mLock);9798Array<ProfileThread *>::iterator i = std::find(mThreads.begin(), mThreads.end(), inThread);99JPH_ASSERT(i != mThreads.end());100mThreads.erase(i);101}102103void Profiler::sAggregate(int inDepth, uint32 inColor, ProfileSample *&ioSample, const ProfileSample *inEnd, Aggregators &ioAggregators, KeyToAggregator &ioKeyToAggregator)104{105// Store depth106ioSample->mDepth = uint8(min(255, inDepth));107108// Update color109if (ioSample->mColor == 0)110ioSample->mColor = inColor;111else112inColor = ioSample->mColor;113114// Start accumulating totals115uint64 cycles_this_with_children = ioSample->mEndCycle - ioSample->mStartCycle;116117// Loop over following samples until we find a sample that starts on or after our end118ProfileSample *sample;119for (sample = ioSample + 1; sample < inEnd && sample->mStartCycle < ioSample->mEndCycle; ++sample)120{121JPH_ASSERT(sample[-1].mStartCycle <= sample->mStartCycle);122JPH_ASSERT(sample->mStartCycle >= ioSample->mStartCycle);123JPH_ASSERT(sample->mEndCycle <= ioSample->mEndCycle);124125// Recurse and skip over the children of this child126sAggregate(inDepth + 1, inColor, sample, inEnd, ioAggregators, ioKeyToAggregator);127}128129// Find the aggregator for this name / filename pair130Aggregator *aggregator;131KeyToAggregator::iterator aggregator_idx = ioKeyToAggregator.find(ioSample->mName);132if (aggregator_idx == ioKeyToAggregator.end())133{134// Not found, add to map and insert in array135ioKeyToAggregator.try_emplace(ioSample->mName, ioAggregators.size());136ioAggregators.emplace_back(ioSample->mName);137aggregator = &ioAggregators.back();138}139else140{141// Found142aggregator = &ioAggregators[aggregator_idx->second];143}144145// Add the measurement to the aggregator146aggregator->AccumulateMeasurement(cycles_this_with_children);147148// Update ioSample to the last child of ioSample149JPH_ASSERT(sample[-1].mStartCycle <= ioSample->mEndCycle);150JPH_ASSERT(sample >= inEnd || sample->mStartCycle >= ioSample->mEndCycle);151ioSample = sample - 1;152}153154void Profiler::DumpInternal()155{156// Freeze data from threads157// Note that this is not completely thread safe: As a profile sample is added mCurrentSample is incremented158// but the data is not written until the sample finishes. So if we dump the profile information while159// some other thread is running, we may get some garbage information from the previous frame160Threads threads;161for (ProfileThread *t : mThreads)162threads.push_back({ t->mThreadName, t->mSamples, t->mSamples + t->mCurrentSample });163164// Shift all samples so that the first sample is at zero165uint64 min_cycle = 0xffffffffffffffffUL;166for (const ThreadSamples &t : threads)167if (t.mSamplesBegin < t.mSamplesEnd)168min_cycle = min(min_cycle, t.mSamplesBegin[0].mStartCycle);169for (const ThreadSamples &t : threads)170for (ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)171{172s->mStartCycle -= min_cycle;173s->mEndCycle -= min_cycle;174}175176// Determine tag of this profile177String tag;178if (mDumpTag.empty())179{180// Next sequence number181static int number = 0;182++number;183tag = ConvertToString(number);184}185else186{187// Take provided tag188tag = mDumpTag;189mDumpTag.clear();190}191192// Aggregate data across threads193Aggregators aggregators;194KeyToAggregator key_to_aggregators;195for (const ThreadSamples &t : threads)196for (ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)197sAggregate(0, Color::sGetDistinctColor(0).GetUInt32(), s, end, aggregators, key_to_aggregators);198199// Dump as chart200DumpChart(tag.c_str(), threads, key_to_aggregators, aggregators);201}202203static String sHTMLEncode(const char *inString)204{205String str(inString);206StringReplace(str, "<", "<");207StringReplace(str, ">", ">");208return str;209}210211void Profiler::DumpChart(const char *inTag, const Threads &inThreads, const KeyToAggregator &inKeyToAggregators, const Aggregators &inAggregators)212{213// Open file214std::ofstream f;215f.open(StringFormat("profile_chart_%s.html", inTag).c_str(), std::ofstream::out | std::ofstream::trunc);216if (!f.is_open())217return;218219// Write header220f << R"(<!DOCTYPE html>221<html>222<head>223<title>Profile Chart</title>224<style>225html, body {226padding: 0px;227border: 0px;228margin: 0px;229width: 100%;230height: 100%;231overflow: hidden;232}233234canvas {235position: absolute;236top: 10px;237left: 10px;238padding: 0px;239border: 0px;240margin: 0px;241}242243#tooltip {244font: Courier New;245position: absolute;246background-color: white;247border: 1px;248border-style: solid;249border-color: black;250pointer-events: none;251padding: 5px;252font: 14px Arial;253visibility: hidden;254height: auto;255}256257.stat {258color: blue;259text-align: right;260}261</style>262<script type="text/javascript">263var canvas;264var ctx;265var tooltip;266var min_scale;267var scale;268var offset_x = 0;269var offset_y = 0;270var size_y;271var dragging = false;272var previous_x = 0;273var previous_y = 0;274var bar_height = 15;275var line_height = bar_height + 2;276var thread_separation = 6;277var thread_font_size = 12;278var thread_font = thread_font_size + "px Arial";279var bar_font_size = 10;280var bar_font = bar_font_size + "px Arial";281var end_cycle = 0;282283function drawChart()284{285ctx.clearRect(0, 0, canvas.width, canvas.height);286287ctx.lineWidth = 1;288289var y = offset_y;290291for (var t = 0; t < threads.length; t++)292{293// Check if thread has samples294var thread = threads[t];295if (thread.start.length == 0)296continue;297298// Draw thread name299y += thread_font_size;300ctx.font = thread_font;301ctx.fillStyle = "#000000";302ctx.fillText(thread.thread_name, 0, y);303y += thread_separation;304305// Draw outlines for each bar of samples306ctx.fillStyle = "#c0c0c0";307for (var d = 0; d <= thread.max_depth; d++)308ctx.fillRect(0, y + d * line_height, canvas.width, bar_height);309310// Draw samples311ctx.font = bar_font;312for (var s = 0; s < thread.start.length; s++)313{314// Cull bar315var rx = scale * (offset_x + thread.start[s]);316if (rx > canvas.width) // right of canvas317break;318var rw = scale * thread.cycles[s];319if (rw < 0.5) // less than half pixel, skip320continue;321if (rx + rw < 0) // left of canvas322continue;323324// Draw bar325var ry = y + line_height * thread.depth[s];326ctx.fillStyle = thread.color[s];327ctx.fillRect(rx, ry, rw, bar_height);328ctx.strokeStyle = thread.darkened_color[s];329ctx.strokeRect(rx, ry, rw, bar_height);330331// Get index in aggregated list332var a = thread.aggregator[s];333334// Draw text335if (rw > aggregated.name_width[a])336{337ctx.fillStyle = "#000000";338ctx.fillText(aggregated.name[a], rx + (rw - aggregated.name_width[a]) / 2, ry + bar_height - 4);339}340}341342// Next line343y += line_height * (1 + thread.max_depth) + thread_separation;344}345346// Update size347size_y = y - offset_y;348}349350function drawTooltip(mouse_x, mouse_y)351{352var y = offset_y;353354for (var t = 0; t < threads.length; t++)355{356// Check if thread has samples357var thread = threads[t];358if (thread.start.length == 0)359continue;360361// Thead name362y += thread_font_size + thread_separation;363364// Draw samples365for (var s = 0; s < thread.start.length; s++)366{367// Cull bar368var rx = scale * (offset_x + thread.start[s]);369if (rx > mouse_x)370break;371var rw = scale * thread.cycles[s];372if (rx + rw < mouse_x)373continue;374375var ry = y + line_height * thread.depth[s];376if (mouse_y >= ry && mouse_y < ry + bar_height)377{378// Get index into aggregated list379var a = thread.aggregator[s];380381// Found bar, fill in tooltip382tooltip.style.left = (canvas.offsetLeft + mouse_x) + "px";383tooltip.style.top = (canvas.offsetTop + mouse_y) + "px";384tooltip.style.visibility = "visible";385tooltip.innerHTML = aggregated.name[a] + "<br>"386+ "<table>"387+ "<tr><td>Time:</td><td class=\"stat\">" + (1000000 * thread.cycles[s] / cycles_per_second).toFixed(2) + " µs</td></tr>"388+ "<tr><td>Start:</td><td class=\"stat\">" + (1000000 * thread.start[s] / cycles_per_second).toFixed(2) + " µs</td></tr>"389+ "<tr><td>End:</td><td class=\"stat\">" + (1000000 * (thread.start[s] + thread.cycles[s]) / cycles_per_second).toFixed(2) + " µs</td></tr>"390+ "<tr><td>Avg. Time:</td><td class=\"stat\">" + (1000000 * aggregated.cycles_per_frame[a] / cycles_per_second / aggregated.calls[a]).toFixed(2) + " µs</td></tr>"391+ "<tr><td>Min Time:</td><td class=\"stat\">" + (1000000 * aggregated.min_cycles[a] / cycles_per_second).toFixed(2) + " µs</td></tr>"392+ "<tr><td>Max Time:</td><td class=\"stat\">" + (1000000 * aggregated.max_cycles[a] / cycles_per_second).toFixed(2) + " µs</td></tr>"393+ "<tr><td>Time / Frame:</td><td class=\"stat\">" + (1000000 * aggregated.cycles_per_frame[a] / cycles_per_second).toFixed(2) + " µs</td></tr>"394+ "<tr><td>Calls:</td><td class=\"stat\">" + aggregated.calls[a] + "</td></tr>"395+ "</table>";396return;397}398}399400// Next line401y += line_height * (1 + thread.max_depth) + thread_separation;402}403404// No bar found, hide tooltip405tooltip.style.visibility = "hidden";406}407408function onMouseDown(evt)409{410dragging = true;411previous_x = evt.clientX, previous_y = evt.clientY;412tooltip.style.visibility = "hidden";413}414415function onMouseUp(evt)416{417dragging = false;418}419420function clampMotion()421{422// Clamp horizontally423var min_offset_x = canvas.width / scale - end_cycle;424if (offset_x < min_offset_x)425offset_x = min_offset_x;426if (offset_x > 0)427offset_x = 0;428429// Clamp vertically430var min_offset_y = canvas.height - size_y;431if (offset_y < min_offset_y)432offset_y = min_offset_y;433if (offset_y > 0)434offset_y = 0;435436// Clamp scale437if (scale < min_scale)438scale = min_scale;439var max_scale = 1000 * min_scale;440if (scale > max_scale)441scale = max_scale;442}443444function onMouseMove(evt)445{446if (dragging)447{448// Calculate new offset449offset_x += (evt.clientX - previous_x) / scale;450offset_y += evt.clientY - previous_y;451452clampMotion();453454drawChart();455}456else457drawTooltip(evt.clientX - canvas.offsetLeft, evt.clientY - canvas.offsetTop);458459previous_x = evt.clientX, previous_y = evt.clientY;460}461462function onScroll(evt)463{464tooltip.style.visibility = "hidden";465466var old_scale = scale;467if (evt.deltaY > 0)468scale /= 1.1;469else470scale *= 1.1;471472clampMotion();473474// Ensure that event under mouse stays under mouse475var x = previous_x - canvas.offsetLeft;476offset_x += x / scale - x / old_scale;477478clampMotion();479480drawChart();481}482483function darkenColor(color)484{485var i = parseInt(color.slice(1), 16);486487var r = i >> 16;488var g = (i >> 8) & 0xff;489var b = i & 0xff;490491r = Math.round(0.8 * r);492g = Math.round(0.8 * g);493b = Math.round(0.8 * b);494495i = (r << 16) + (g << 8) + b;496497return "#" + i.toString(16);498}499500function startChart()501{502// Fetch elements503canvas = document.getElementById('canvas');504ctx = canvas.getContext("2d");505tooltip = document.getElementById('tooltip');506507// Resize canvas to fill screen508canvas.width = document.body.offsetWidth - 20;509canvas.height = document.body.offsetHeight - 20;510511// Register mouse handlers512canvas.onmousedown = onMouseDown;513canvas.onmouseup = onMouseUp;514canvas.onmouseout = onMouseUp;515canvas.onmousemove = onMouseMove;516canvas.onwheel = onScroll;517518for (var t = 0; t < threads.length; t++)519{520var thread = threads[t];521522// Calculate darkened colors523thread.darkened_color = new Array(thread.color.length);524for (var s = 0; s < thread.color.length; s++)525thread.darkened_color[s] = darkenColor(thread.color[s]);526527// Calculate max depth and end cycle528thread.max_depth = 0;529for (var s = 0; s < thread.start.length; s++)530{531thread.max_depth = Math.max(thread.max_depth, thread.depth[s]);532end_cycle = Math.max(end_cycle, thread.start[s] + thread.cycles[s]);533}534}535536// Calculate width of name strings537ctx.font = bar_font;538aggregated.name_width = new Array(aggregated.name.length);539for (var a = 0; a < aggregated.name.length; a++)540aggregated.name_width[a] = ctx.measureText(aggregated.name[a]).width;541542// Store scale properties543min_scale = canvas.width / end_cycle;544scale = min_scale;545546drawChart();547}548</script>549</head>550<body onload="startChart();">551<script type="text/javascript">552)";553554// Get cycles per second555uint64 cycles_per_second = GetProcessorTicksPerSecond();556f << "var cycles_per_second = " << cycles_per_second << ";\n";557558// Dump samples559f << "var threads = [\n";560bool first_thread = true;561for (const ThreadSamples &t : inThreads)562{563if (!first_thread)564f << ",\n";565first_thread = false;566567f << "{\nthread_name: \"" << t.mThreadName << "\",\naggregator: [";568bool first = true;569for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)570{571if (!first)572f << ",";573first = false;574f << inKeyToAggregators.find(s->mName)->second;575}576f << "],\ncolor: [";577first = true;578for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)579{580if (!first)581f << ",";582first = false;583Color c(s->mColor);584f << StringFormat("\"#%02x%02x%02x\"", c.r, c.g, c.b);585}586f << "],\nstart: [";587first = true;588for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)589{590if (!first)591f << ",";592first = false;593f << s->mStartCycle;594}595f << "],\ncycles: [";596first = true;597for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)598{599if (!first)600f << ",";601first = false;602f << s->mEndCycle - s->mStartCycle;603}604f << "],\ndepth: [";605first = true;606for (const ProfileSample *s = t.mSamplesBegin, *end = t.mSamplesEnd; s < end; ++s)607{608if (!first)609f << ",";610first = false;611f << int(s->mDepth);612}613f << "]\n}";614}615616// Dump aggregated data617f << "];\nvar aggregated = {\nname: [";618bool first = true;619for (const Aggregator &a : inAggregators)620{621if (!first)622f << ",";623first = false;624String name = "\"" + sHTMLEncode(a.mName) + "\"";625f << name;626}627f << "],\ncalls: [";628first = true;629for (const Aggregator &a : inAggregators)630{631if (!first)632f << ",";633first = false;634f << a.mCallCounter;635}636f << "],\nmin_cycles: [";637first = true;638for (const Aggregator &a : inAggregators)639{640if (!first)641f << ",";642first = false;643f << a.mMinCyclesInCallWithChildren;644}645f << "],\nmax_cycles: [";646first = true;647for (const Aggregator &a : inAggregators)648{649if (!first)650f << ",";651first = false;652f << a.mMaxCyclesInCallWithChildren;653}654f << "],\ncycles_per_frame: [";655first = true;656for (const Aggregator &a : inAggregators)657{658if (!first)659f << ",";660first = false;661f << a.mTotalCyclesInCallWithChildren;662}663664// Write footer665f << R"(]};666</script>667668<canvas id="canvas"></canvas>669<div id="tooltip"></div>670671</tbody></table></body></html>)";672}673674#endif // JPH_PROFILE_ENABLED675676JPH_NAMESPACE_END677678679