Files
Ez2Lazer/osu.Game/LAsEzExtensions/Analysis/EzAnalysisWarmupProcessor.cs
LA 0b9f9f70d6 主要为代码质量更新
1. 匹配新版按钮控件的自动宽度写法

2. 统一Ez日志写入方向

3.移除历史修改:缓存启用mod列表,切换mod时保持通用mod开启状态

4.代码格式化、

5.修改文件名称表意,更直观
2026-03-12 19:29:55 +08:00

285 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Performance;
using osu.Game.Screens.Play;
namespace osu.Game.LAsEzExtensions.Analysis
{
/// <summary>
/// 在启动阶段后台预热 mania analysis 缓存。
/// 机制对齐官方 <see cref="BackgroundDataStoreProcessor"/>
/// - 使用 <see cref="ProgressNotification"/> 展示进度并允许用户取消。
/// - 游戏进行中 / 高性能会话期间自动 sleep避免抢占。
/// - 作为长任务在后台运行,不阻塞启动。
/// </summary>
public partial class EzAnalysisWarmupProcessor : Component
{
protected Task ProcessingTask { get; private set; } = null!;
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private RealmAccess realmAccess { get; set; } = null!;
[Resolved]
private EzAnalysisCache maniaAnalysisCache { get; set; } = null!;
[Resolved]
private EzAnalysisPersistentStore persistentStore { get; set; } = null!;
[Resolved(CanBeNull = true)]
private INotificationOverlay? notificationOverlay { get; set; }
[Resolved]
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
[Resolved]
private IHighPerformanceSessionManager? highPerformanceSessionManager { get; set; }
protected virtual int TimeToSleepDuringGameplay => 30000;
protected override void LoadComplete()
{
base.LoadComplete();
ProcessingTask = Task.Factory.StartNew(populateManiaAnalysis, TaskCreationOptions.LongRunning).ContinueWith(t =>
{
if (t.Exception?.InnerException is ObjectDisposedException)
{
Logger.Log("Finished mania analysis warmup aborted during shutdown");
return;
}
Logger.Log("Finished background mania analysis warmup!");
});
}
private void populateManiaAnalysis()
{
if (!EzAnalysisPersistentStore.Enabled)
{
Logger.Log("Mania analysis persistence is disabled; skipping warmup.", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
return;
}
List<(Guid id, string hash)> beatmaps = new List<(Guid id, string hash)>();
Logger.Log("Querying for mania beatmaps to warm up analysis cache...", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
realmAccess.Run(r =>
{
int totalBeatmaps = 0;
int totalWithSet = 0;
int maniaTotal = 0;
int maniaWithSet = 0;
int maniaHidden = 0;
Dictionary<string, int> rulesetDistribution = new Dictionary<string, int>();
// Align with official BackgroundDataStoreProcessor: don't exclude Hidden beatmaps here.
// Hidden beatmaps can still be attached to sets and may contribute to cache hit rates.
foreach (var b in r.All<BeatmapInfo>())
{
totalBeatmaps++;
bool isMania = b.Ruleset.OnlineID == 3;
if (isMania)
{
maniaTotal++;
if (b.Hidden)
maniaHidden++;
}
if (totalBeatmaps <= 2000)
{
string key = $"{b.Ruleset.ShortName}:{b.Ruleset.OnlineID}";
rulesetDistribution.TryGetValue(key, out int count);
rulesetDistribution[key] = count + 1;
}
if (b.BeatmapSet == null)
continue;
totalWithSet++;
if (!isMania)
continue;
maniaWithSet++;
beatmaps.Add((b.ID, b.Hash));
}
Logger.Log($"Warmup beatmap query summary: total={totalBeatmaps}, total_with_set={totalWithSet}, mania_total={maniaTotal}, mania_with_set={maniaWithSet}, mania_hidden={maniaHidden}",
Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
if (maniaTotal == 0)
{
string dist = string.Join(", ", rulesetDistribution.OrderByDescending(kvp => kvp.Value).Take(10).Select(kvp => $"{kvp.Key}={kvp.Value}"));
Logger.Log($"Warmup beatmap ruleset distribution (first 2000): {dist}", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
}
});
if (beatmaps.Count == 0)
return;
// 增量:只重算缺失/过期hash/version 不匹配)的条目。
// 与官方 star rating 进度通知一致:只在确实需要做“预计算”时展示一条 ProgressNotification。
var needingRecompute = persistentStore.GetBeatmapsNeedingRecompute(beatmaps);
if (needingRecompute.Count == 0)
{
Logger.Log("No beatmaps require mania analysis warmup.", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
return;
}
Logger.Log($"Found {needingRecompute.Count} beatmaps which require mania analysis warmup.", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
Logger.Log($"Starting mania analysis warmup. total={needingRecompute.Count}", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
var notification = showProgressNotification(needingRecompute.Count, "Precomputing mania analysis for beatmaps", "beatmaps' mania analysis has been precomputed");
int processedCount = 0;
int failedCount = 0;
foreach (Guid id in needingRecompute)
{
if (notification?.State == ProgressNotificationState.Cancelled)
break;
updateNotificationProgress(notification, processedCount, needingRecompute.Count);
sleepIfRequired();
var beatmap = realmAccess.Run(r => r.Find<BeatmapInfo>(id)?.Detach());
if (beatmap == null)
{
++failedCount;
continue;
}
try
{
// 仅预热 no-mod 的缓存项:与官方 star 预计算一致(基础值持久化/复用modded 部分仍按需计算。
// 关键warmup 绝不阻塞可见项。
// - 等待当前所有高优先级(可见项)计算完成后再启动 warmup。
// - 启用持久化时,仅预热 SQLite不把所有结果长期留在内存缓存里。
maniaAnalysisCache.WarmupPersistentOnlyAsync(beatmap, CancellationToken.None)
.GetAwaiter()
.GetResult();
++processedCount;
}
catch (Exception e)
{
Logger.Log($"Background mania analysis warmup failed on {beatmap}: {e}", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
++failedCount;
}
if (processedCount % 50 == 0)
((IWorkingBeatmapCache)beatmapManager).Invalidate(beatmap);
}
completeNotification(notification, processedCount, needingRecompute.Count, failedCount);
}
private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
{
if (notification == null)
return;
Schedule(() =>
{
notification.Text = notification.Text.ToString().Split('(').First().TrimEnd() + $" ({processedCount} of {totalCount})";
notification.Progress = (float)processedCount / totalCount;
});
if (processedCount % 100 == 0)
Logger.Log($"Warmup progress: {processedCount} of {totalCount}");
}
private void completeNotification(ProgressNotification? notification, int processedCount, int totalCount, int failedCount)
{
if (notification == null)
return;
Schedule(() =>
{
if (processedCount == totalCount)
{
notification.CompletionText = $"{processedCount} {notification.CompletionText}";
notification.Progress = 1;
notification.State = ProgressNotificationState.Completed;
}
else
{
notification.Text = $"{processedCount} of {totalCount} {notification.CompletionText}";
if (failedCount > 0)
notification.Text += $" Check logs for issues with {failedCount} failed items.";
notification.State = ProgressNotificationState.Cancelled;
}
});
}
private ProgressNotification? showProgressNotification(int totalCount, string running, string completed)
{
if (notificationOverlay == null)
{
Logger.Log("INotificationOverlay is null; mania analysis warmup progress notification will not be shown.", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
return null;
}
if (totalCount <= 0)
return null;
ProgressNotification notification = new ProgressNotification
{
Text = running,
CompletionText = completed,
State = ProgressNotificationState.Active
};
Schedule(() =>
{
try
{
notificationOverlay?.Post(notification);
Logger.Log("Posted mania analysis warmup progress notification.", Ez2ConfigManager.LOGGER_NAME, LogLevel.Important);
}
catch (Exception e)
{
Logger.Error(e, "Failed to post mania analysis warmup notification.");
}
});
return notification;
}
private void sleepIfRequired()
{
while (localUserPlayInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying || highPerformanceSessionManager?.IsSessionActive == true)
{
Logger.Log("Mania analysis warmup sleeping due to active gameplay...");
Thread.Sleep(TimeToSleepDuringGameplay);
}
}
}
}