Suggestion to extend MicroProfilerService to be usable

As a developer, it can be too tedious to test performance with different subsets of your playerbase, especially with mobile players due to how microprofiling works.

I’m proposing a new solution that allows developers to toggle a setting to allow players / specific players to submit their own microprofiler dumps from the game itself and an integration into the configuration of a game allowing developers to analyze it for performance issues.

Microprofiler dumps over 14 days old will be cleared to conserve storage and to preserve recency.
Microprofiler dumps might not be able to be sent by <13’s? (I’m presuming because of data laws?)

While I’m hoping that this is able to cover most cases for a developer, it may not be enough and I’m absolutely open to receiving feedback in that case, and I wouldn’t be surprised as this is unintuitive, as this is how I’d personally think about it.

Possibly a new Instance (DeviceInfo?), while reading other threads, specifically CaptureService:GetDeviceInfo() changes, I’ve noticed that fingerprinting is a huge issue, so here’s my suggestion to allow developers usable information while preventing fingerprinting:
Each Snapshot will only contain the user’s currently used ram, platform (Mobile, Computer, Console, Quest) and a generic descriptor of their system (Entry-Level Platform, Standard Platform and High-End Platform, these of course would only apply to PC and Mobile.)

A new event under MicroProfilerService called .FrameCollected with parameters for the delta of the last frame and also passes along a MicroprofilerSnapshot (as shown below), if the MicroprofilerSnapshot is not stored at the end of the event, it is discarded.

A new event on the server called .MicroprofilerUpload(), which the server uses, it passes along the parameters as Player? (can be nil if the player chooses to remain anonymous), MicroprofilerSnapshotContainer and DeviceInfo? (can be nil if the player chooses not to disclose this information.)

A new Instance (MicroprofilerSnapshot?) (this might end up literally being just what is essentially a dictionary) that stores Microprofiler information about the current frame with methods such as:
:DebugTagPresent(string) which would take a string from Microprofiler’s debug label and return true or false if it is present in the frame stemming from .FrameCollected (debug.profilebegin()),
:FunctionCalled(Script? | ModuleScript? | LocalScript?, string) this method would take a script instance and a label to check if a certain function has ran.

A new Instance (MicroprofilerSnapshotContainer?) created via MicroProfilerService:CreateSnapshotContainer() and options (example: {MaxFrames = 128} that stores MicroprofilerSnapshots, this could literally just be a table, however I figure a new Instance would make more sense.
This would include Methods such as :Insert(MicroprofilerSnapshot) :Remove(Index), :Clear() and :Destroy()

Methods on the client and server under MicroProfilerService
MicroProfilerService:UploadCapture(Player?, MicroprofilerSnapShotContainer?), if the client calls this, it will upload their container to the server and trigger .MicroprofilerUpload, if the server calls this it will upload their container to be viewed via the creator hub.

Here’s how I’d like to imagine how this would work in code:

-- Client
local MicroProfilerService = game:GetService("MicroProfilerService")
local Container = MicroProfilerService:CreateSnapshotContainer({MaxFrames = 128}) -- Must be a multiple of 2.

local PreviousDelta = nil
local Tolerance = 0.45

MicroProfilerService.FrameCollected:Connect(function(delta : number, Snapshot : MicroprofilerSnapshot)
	local PercentChanged = 0
	local Stored = false -- dumb bandaid idea I had, this could probably be handled internally by giving each Snapshot an ID and checking when Insert is called.

	if PreviousDelta then
		PercentChanged = delta/PreviousDelta
	end

	if Snapshot:DebugTagPresent("Expensive code we're tracking!") then
		Container:Insert(Snapshot)
		Stored = true
	end

	if Snapshot:FunctionCalled(script, "ExpensiveCode") and not Stored then
		Container:Insert(Snapshot)
		Stored = true
	end


	if PercentChanged > Tolerance and not Stored then
		Container:Insert(Snapshot)
		Stored = true
	end
	
	PreviousDelta = delta
end)
-- Server
local MicroProfilerService = game:GetService("MicroProfilerService")

MicroProfilerService.MicroprofilerUpload:Connect(function(Player : Player? | nil, Container : MicroprofilerSnapshotContainer, DeviceInfo : DeviceInfo? | nil)
	-- As it stands right now, my idea could be susceptible to exploits, so perhaps some basic checks unless they can be handled internally?
	-- Realistically, I don't think there's a need for this function, so it could be scrapped entirely?
	Container:UploadCapture(Player, Container, DeviceInfo)
end)

This idea could use more fleshing out, I’ve spent about the past 45 minutes on and off thinking about this, so if you have any edge cases or anything that could pose an issue, please let me know and I’d be happy to let you know what I think could solve the issue.

10 Likes

Hey Vainglory, great idea and well-thought-out concrete details - thanks for that!
Splitting device hardware info into performance buckets, the snapshot container and its max capacity, DebugTagPresent - these are the things that resonate with me the most.

There is a thread where I described my vision for this, and some work is underway on implementing it:

We have a policy not to show work that isn’t ready to ship, say, tomorrow, otherwise I’d tease you with some WIP content.

UploadCapture is definitely on my mind, but that’s for later stages. Before that, there might be an option to grab container data as an array of bytes (a buffer object) and send it over HttpService to your private storage server. Based on your experience, could this be usable in practice? (This will also make you cautious about how many captures you upload, haha)

The possibility of filtering frame snapshots before inserting them into a storage container is interesting. I think that when you detect a frame-time spike, you’d also want to include at least one frame before the spike and one frame after it to get fuller context - which makes the logic a bit more complex.

This also means that your container can hold multiple unrelated chunks of gameplay, possibly separated by minutes of time. It would probably make sense to review them separately later, rather than in a single merged timeline where all frames go one after another without gaps.

Another interesting point is what to do with these dumps afterward. Suppose triggers in the code fire and you end up with 1000 dumps in an hour - do you have ideas about the pipeline for working with them?

My view is that there should be a way both to inspect them visually and to analyze them programmatically outside Roblox. The north star for me here is that you could use something like
Container:GetLastFrame():DebugTagPresent(“MyExpensiveCodeTag”)
both inside Roblox Luau on the fly and outside of Roblox in command-line Luau (or JS/Python) for dumps that you collected as files.

So yeah - great proposal, and stay tuned for updates!

8 Likes

Can we be blessed with API for this use case please

2 Likes

Ideally I’d like to keep sending dumps only accessible to a selective few players, perhaps developers adding conditions such as playtime / certain flags in player data / allowed players (such as testers) would be good?
I could also think about sorting frame deltas by cause (such as script name + function) and allowing developers to delete them based off that (if they’ve found the root cause), and or allowing for external storage.
As harsh as it is, I believe rate limiting could be the best idea, alternatively allowing players to manually submit their issues could be the best idea here.

I actually didn’t think about this the most, I think that this could be a feature in .FrameCollected(), perhaps a signal for the next frame could be hooked onto with :Once() if the current frame delta > tolerance, and the old frame could also be passed along regardless, so .FrameCollected would actually be .FrameCollected(delta : number, Old : MicroprofilerSnapshot, Current : MicroprofilerSnapshot, Next : RBXScriptSignal?)

So my example code would actually look like:

-- Client
local MicroProfilerService = game:GetService("MicroProfilerService")
local Container = MicroProfilerService:CreateSnapshotContainer({MaxFrames = 128}) -- Must be a multiple of 2.

local PreviousDelta = nil
local Tolerance = 0.45

MicroProfilerService.FrameCollected:Connect(function(delta : number, Old : MicroprofilerSnapshot, Current : MicroprofilerSnapshot, Next : RBXScriptSignal?)
	local PercentChanged = 0
	local Stored = false -- dumb bandaid idea I had, this could probably be handled internally by giving each Snapshot an ID and checking when Insert is called.

	if PreviousDelta then
		PercentChanged = delta/PreviousDelta
	end

	if Current:DebugTagPresent("Expensive code we're tracking!") then
		Container:Insert(Snapshot)
		Stored = true
	end

	if Current:FunctionCalled(script, "ExpensiveCode") and not Stored then
		Container:Insert(Current)
		Stored = true
	end


	if PercentChanged > Tolerance and not Stored then
		Container:Insert(Current)
		Stored = true

Next:Once(function(delta : number, NewSnapshot : MicroprofilerSnapshot) -- sorry for poor formatting, I don't feel like getting in studio to format it correctly.
Container:Insert(NewSnapshot)
end)
 Container:Insert(Old)
	end
	
	PreviousDelta = delta
end)

I actually don’t know, I’ve never really used a private storage server, but regardless of my experience this sounds like a great feature for developers who use external services for the platform, I believe grabbing as a buffer would be a great option for more experienced developers looking to keep access to their microprofiler logs.

I’m really glad to hear about your ideas and look forward to seeing whats implemented for this issue.

2 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.