Skip to content

Create a component for headlessly processing replays#36943

Open
LiquidPL wants to merge 6 commits intoppy:masterfrom
LiquidPL:headless-replay
Open

Create a component for headlessly processing replays#36943
LiquidPL wants to merge 6 commits intoppy:masterfrom
LiquidPL:headless-replay

Conversation

@LiquidPL
Copy link
Copy Markdown
Contributor

@LiquidPL LiquidPL commented Mar 11, 2026

Meant to be used on the results screen to automatically process the replay to obtain hit event data for the statistics panel.

RFC. This is kinda rough, what I've done here is effectively taking the essential bits from Player (DrawableRuleset, ScoreProcessor, HealthProcessor) that are needed to properly play through and judge a beatmap (and probably commiting some cardinal sins of threading while at it). This isn't currently plugged in anywhere, as I wanted to ensure this is the right direction to take this first.

I suppose there's no way around having it process and draw all the hitobjects, even if the player components itself is not shown. I was wondering if there's a way to speed this process up somehow, since at least for me processing ~3 minute beatmap would take around 2-3 seconds, which is unfortunate since if this were faster maybe it'd be possible to quickly run this when the user is entering the statistics screen.

@peppy peppy self-requested a review March 12, 2026 06:24

AddStep("import replay", () =>
{
using (var replayStream = TestResources.OpenResource($"Replays/osu-renatus-replay-failed.osr"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resource not committed.

});
AddStep("create player", () =>
{
// Component isn't hidden in tests to allow for visually checking if everything works correctly.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see anything running the test though

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot to remove this thing where I was trying to see if preventing things from rendering would improve the playback speed (it didn't). f753901

@peppy
Copy link
Copy Markdown
Member

peppy commented Mar 12, 2026

Profiling shows some room for improvement.

  • The easiest win is fixing Triangle's use of SortedList, taking a whopping 500 milliseconds of the total 1,777 ms spent in headless frame updates. I think it's defaulting to the triangles skin, causing a lot of usage of this. cc @EVAST9919 you might be interested in this low hanging fruit:
JetBrains Rider-EAP 2026-03-12 at 08 41 28
  • Fetches of StopwatchClock.CurrentTime take up 25% of time. This is likely InterpolatingFramedClock, which is unneeded in this context. If you can figure how to replace the GameplayClockContainer's clock with something much more mechanics that would improve things greatly.
JetBrains Rider-EAP 2026-03-12 at 09 46 58
  • Next up would likely be reassessing FrameStabilityContainer. Right now it's performing a maximum of 10 ms of work per frame, which will limit processing speed at anything below 100 fps. This doesn't seem to affect headless profiling, but affect actual performance in-game.
  • Another improvement may be to make skin(s) specifically used for this purpose that just does zero transform logic. Or somehow conveying to skins that we are operating headless.

But all in all, I don't think these are blockers. We can improve performance as we go. It's already in a usable place IMO, as long as it works well and can be cancelled.

@LiquidPL
Copy link
Copy Markdown
Contributor Author

I think it's defaulting to the triangles skin

That's weird, it's been always using argon for me (at least in the test browser, not when actually running headless). It's definitely a good idea to enforce a skin here though, even if it's not one that's optimized for it yet.

can be cancelled

This is something I'm not fully confident about. Given that what's done currently is just seeking to the end of the replay and waiting for the playback to complete, I don't have a simple way to hook in and interrupt that process. However, since this component isn't meant to be reused after playback starts, I suppose it's fine if whoever owns this just disposes it whenever there's a need to cancel?

Comment on lines +150 to +161
public void Start()
{
if (!IsLoaded)
return;

double? lastFrameTime = score.Replay.Frames.LastOrDefault()?.Time;

if (lastFrameTime == null)
return;

clock.Seek(lastFrameTime.Value);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given what I said in the comment earlier, I suppose this should just run in LoadComplete() and not be exposed as a public method.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a situation where this wouldn't be called immediately.

@peppy
Copy link
Copy Markdown
Member

peppy commented Mar 12, 2026

This is something I'm not fully confident about. Given that what's done currently is just seeking to the end of the replay and waiting for the playback to complete, I don't have a simple way to hook in and interrupt that process. However, since this component isn't meant to be reused after playback starts, I suppose it's fine if whoever owns this just disposes it whenever there's a need to cancel?

Expire the drawable? Or just forcefully remove it. The important part is that there's a simple API to make all this happen which hopefully isn't manual drawable lifetime management every time this is used.

That's weird, it's been always using argon for me (at least in the test browser, not when actually running headless). It's definitely a good idea to enforce a skin here though, even if it's not one that's optimized for it yet.

I only ran headless, and couldn't see in test browser anyway.

@TaterToes

This comment was marked as spam.

peppy pushed a commit that referenced this pull request Mar 13, 2026
As [per
comment](#36943 (comment)).
Unfortunately I'm not able to reproduce such a big impact of triangles
using linked pr (or at all really). In my case usage was low in the
first place and went from 0.9% to 0.6% with this pr, so outside
benchmarking would be great

Improvement list:
* No more sorted list. Basic list is being used which is sorted once
after all the triangles added.
* Out-of-bounds triangles are reused rather than removed and re-added.
* On `Reset` if `AimCount` stays the same, all triangles are reused
instead of being cleared and re-added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants