Internal Profiling
Profiling API
The cohtml::Library
exposes a profiling API that can be used in Development builds of Gameface.
Overview
The method cohtml::Library::EnableProfling
can be used to start and stop the emitting of internal profiling markers. The profiling markers can be used to better understand what Gameface is doing and where its processing time is spent. For more in depth explanation of what happens internally in Gameface, see the profiling and optimization guide. In there we give an overview of what profiling markers there are and what they show.
Different platforms use different profiling markers.
- on PS4/PS5/Xbox One/Xbox Series X/S - Gameface uses the platform native profiling markers
- on other platforms like Desktop, Mobile, and Nintendo Switch - Gameface uses minitrace to emit events that are written to a JSON file and then can be inspected in tools like Chrome’s
chrome://tracing/
tab or Perfetto
Example of Minitrace performance recording visualized in Chrome through chrome://tracing/
:
How to use
cohtml::Library::EnableProfling
accepts a file path as an argument. The file path will be used when profiling through minitrace and Gameface will create a JSON file that will contain all profiling messages. For example, to illustrate the profiling on Windows:
The level
argument of cohtml::Library::EnableProfling
gives the client code to select a profiling level that affects how much profiling markers Gameface will emit:
'level=1
- only basic markers are emitted. This can be used for high-level performance investigation of major Gameface’s systems'level=2
- enables all profiling markers. This can negatively impact performance but provides a lot more information for low-level investigations.
Gameface needs to open a file on the file system when the profiling is done through minitrace. The file opening and writing facility can be provided through the cohtml::LibraryParamsWithExternalRenderingLibrary::FileSystemWriter
field when initiating the cohtml::Library
. This way the user code can provide a custom file writer and all file operations of Gameface will go through this object.
On the desktop platforms – Windows, Mac, and Linux – Gameface ships with an internal file writer that works with fopen
, fwrite
/, and fclose
. If this is undesirable, the user code has to provide a custom IFileSystemWriter
object.
The basic usage of the new API goes as follows:
// example implementation of the cohtml::ISyncStreamWriter interace. This implementation
// can be used as guideline if the default internal "cohtml::ISyncStreamWriter"
// is undesirable.
class FileWriter : public cohtml::ISyncStreamWriter
{
public:
FileWriter(const char* path)
{
m_File.open(path, std::ofstream::out | std::ofstream::binary);
}
virtual void Write(const char* data, unsigned count) override
{
if (m_File.is_open())
{
m_File.write(data, count);
}
}
virtual void Close() override
{
if (m_File.is_open())
{
m_File.close();
}
delete this;
}
bool IsOpen()
{
return m_File.is_open();
}
private:
std::ofstream m_File;
};
// Example implementation of the cohtml::IFileSystemWriter. This implementation
// can be used as guideline if the default internal "cohtml::IFileSystemWriter"
// is undesirable.
class FileSystemWriter : public cohtml::IFileSystemWriter
{
public:
virtual cohtml::ISyncStreamWriter* OpenFile(const char* path) override
{
FileWriter* writer = new FileWriter(path);
if (!writer->IsOpen())
{
writer->Close();
return nullptr;
}
return writer;
}
};
static FileSystemWriter fileSystemWriter;
cohtml::LibraryParams params;
...
// replace the default internal "FileSystemWriter"
params.FileSystemWriter = fileSystemWriter;
// uncomment to use the default internal "FileSystemWriter"
// params.FileSystemWriter = nullptr;
cohtml::Library cohtmlLibrary = Library::Initialize(COHTML_LICENSE_KEY, params);
...
unsigned profilngLevel = 2;
cohtmlLibrary->EnableProfiling(true, "profling_output.json", profilngLevel);
...
cohtmlLibrary->EnableProfiling(false, nullptr, 2);
Inspector Profiling vs the new Profiling API
Gameface can also be profiling with its existing integration with the Chrome’s inspector. There are, however, some key differences between the new API and the Recording tab in the inspector.
- The new API is geared more towards profiling of the C++ code. It introduces less overhead and it’s meant for finer level of performance tracing. The reduced overhead is especially true on the platforms where Gameface can be profiled with native profile markers.
- The inspector profiling is the only way to see and profile the execution of JavaScript.
Even though these differences do exist, Gameface remains the same system in both cases. For a better understanding of what Gameface does internally and how to profile it effectively, see this profiling and optimization guide.
Externalizing profiling marker to user code
Gameface also provides the ability to handle raw internal profiling events. The described profiling method above uses different profiling facilities provided by separate libraries – minitrace or native libraries on consoles. Gameface can, however, also be hooked to external profiling tools. In this case, Gameface routes all internal markers through an interface object.
The interface object that facilitates this is cohtml::IProfileMarkersTracer
and can be passed through the cohtml::LibraryParamsWithExternalRenderingLibrary::ProfileMarkersTracer
field during initialization of the cohtml::Library
. Later, when the Gameface profiling is enabled by calling cohtml::Library::EnableProfling
, all internal profiling markers will call methods on the provided cohtml::IProfileMarkersTracer
object.
cohtml::IProfileMarkersTracer
object is provided, Gameface will use the default as mentioned above scheme where minitrace is used on Desktop, Mobile, and Nintendo Switch, and native profiling is used on PS4/PS5/Xbox One/Xbox Series X/S.Here we give an example basic implementation of the cohtml::IProfileMarkersTracer
interface for the Tracy profiler. This implementation can be used to profile Gameface in real-time.
class TracyProfileMarkersTracer : public cohtml::IProfileMarkersTracer
{
public:
// Called when Cohtml begins simple scoped profile events
void BeginTraceEvent(const char* system, const char* markerName) override;
// Called when Cohtml begins simple scoped profile events with a string value attached to them
void BeginTraceEventWithString(const char* system, const char* markerName, const char* valueName, const char* value) override;
// Called when Cohtml begins/ends simple scoped profile events with an integer value attached to them
void BeginTraceEventWithInt(const char* system, const char* markerName, const char* valueName, int value) override;
// Called when Cohtml ends simple scoped profile events
void EndTraceEvent(const char* system, const char* markerName) override;
// Called when Cohtml begins/ends profile events that can cross threads. That is, the event can be started on one thread, but ended
// on another. Those events are very rare
void BeginTraceEventAsync(const char* system, const char* markerName, void* id) override;
void EndTraceEventAsync(const char* system, const char* markerName, void* id) override;
// Called when Cohtml emits profile events that do not have start and beginning but are one off happenings of something
void InstantTraceEvent(const char* system, const char* markerName) override {}
void InstantTraceEventWithInt(const char* system, const char* markerName, const char* valueName, int value) override {}
// Cohtml will hint a thread name depending on the job it is executing by calling this method.
// User code doesn't necessarily have to consider this hint if Cohtml is being profiled in the context of larger systems.
// The hint can be used to specify the thread name in the profiling system
void HintThreadName(const char* name) override {}
// Called when Cohtml updates some profiling counter with a value. Most of the time, those are different memory
// counters for various subsystems of Cohtml.
void UpdateCounter(const char* name, int value) override;
private:
static inline thread_local std::vector<TracyCZoneCtx> m_TracyContexts = {};
};
void TracyProfileMarkersTracer::BeginTraceEvent(const char* system, const char* markerName)
{
// 'loc' usually provides information
// about the source code location of the marker but in this context, this information
// cannot be provided.
auto loc = ___tracy_alloc_srcloc(0, "cohtml.dll", strlen("cohtml.dll"), markerName, strlen(markerName));
TracyCZoneCtx ctx = ___tracy_emit_zone_begin_alloc(loc, true);
___tracy_emit_zone_name(ctx, markerName, strlen(markerName));
// The context object must be used later to end the marker so we save it in
// a vector that will act as a stack
m_TracyContexts.push_back(ctx);
}
void TracyProfileMarkersTracer::BeginTraceEventWithString(const char* system, const char* markerName, const char* valueName, const char* value)
{
// 'loc' usually provides information
// about the source code location of the marker but in this context, this information
// cannot be provided.
auto loc = ___tracy_alloc_srcloc(0, "cohtml.dll", strlen("cohtml.dll"), markerName, strlen(markerName));
TracyCZoneCtx ctx = ___tracy_emit_zone_begin_alloc(loc, true);
___tracy_emit_zone_name(ctx, markerName, strlen(markerName));
// The context object must be used later to end the marker so we save it in
// a vector that will act as a stack
m_TracyContexts.push_back(ctx);
}
void TracyProfileMarkersTracer::BeginTraceEventWithInt(const char* system, const char* markerName, const char* valueName, int value)
{
// 'loc' usually provides information
// about the source code location of the marker but in this context, this information
// cannot be provided.
auto loc = ___tracy_alloc_srcloc(0, "cohtml.dll", strlen("cohtml.dll"), markerName, strlen(markerName));
TracyCZoneCtx ctx = ___tracy_emit_zone_begin_alloc(loc, true);
___tracy_emit_zone_name(ctx, markerName, strlen(markerName));
// The context object must be used later to end the marker so we save it in
// a vector that will act as a stack
m_TracyContexts.push_back(ctx);
}
void TracyProfileMarkersTracer::EndTraceEvent(const char* system, const char* markerName)
{
if (!m_TracyContexts.empty())
{
// Pop the last object from the stack and tell Tracy to end the profile marker
___tracy_emit_zone_end(m_TracyContexts.back());
m_TracyContexts.pop_back();
}
}
void TracyProfileMarkersTracer::UpdateCounter(const char* name, int value)
{
// All of the current Cohtml counters are memory counters so we'll tell Tracy to treat the value as a memory
TracyPlotConfig(name, tracy::PlotFormatType::Memory, true, true, 0);
// Plot the value through the Tracy API
TracyPlot(name, (int64_t)value);
}
....
cohtml::LibraryParams params;
...
static TracyProfileMarkersTracer tracyTracer{};
params.ProfileMarkersTracer = &tracyTracer;
auto coherentLibrary = Library::Initialize(COHTML_LICENSE_KEY, params);
...
// start profiling with L1 markers
coherentLibrary->EnableProfiling(true, nullptr, 1);
For the TracyProfileMarkersTracer
class to work properly, the application has to be compiled with TracyClient.cpp, preferably from an official release of Tracy. After the integration, Gameface can be profiled live with the official Tracy profiler