Internal Profiling

Profiling API

The cohtml::Library exposes a profiling API that can be used in Development builds of Prysm.

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 Prysm is doing and where its processing time is spent. For more in depth explanation of what happens internally in Prysm, 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 - Prysm uses the platform native profiling markers
  • on other platforms like Desktop, Mobile, and Nintendo Switch - Prysm 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 Prysm 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 Prysm will emit:

  • 'level=1 - only basic markers are emitted. This can be used for high-level performance investigation of major Prysm’s systems
  • 'level=2 - enables all profiling markers. This can negatively impact performance but provides a lot more information for low-level investigations.

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

Prysm 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 Prysm 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, Prysm remains the same system in both cases. For a better understanding of what Prysm does internally and how to profile it effectively, see this profiling and optimization guide.

Externalizing profiling marker to user code

Prysm 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. Prysm can, however, also be hooked to external profiling tools. In this case, Prysm 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 Prysm profiling is enabled by calling cohtml::Library::EnableProfling, all internal profiling markers will call methods on the provided cohtml::IProfileMarkersTracer object.

Here we give an example basic implementation of the cohtml::IProfileMarkersTracer interface for the Tracy profiler. This implementation can be used to profile Prysm 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, Prysm can be profiled live with the official Tracy profiler