diff --git a/backend/src/MoShiro.Backend.Application/Ports/StorePorts.cs b/backend/src/MoShiro.Backend.Application/Ports/StorePorts.cs index 7f1e666..b5f7aa8 100644 --- a/backend/src/MoShiro.Backend.Application/Ports/StorePorts.cs +++ b/backend/src/MoShiro.Backend.Application/Ports/StorePorts.cs @@ -213,6 +213,9 @@ public interface IMetricStore /// 这条序列在某一层里最早的窗口起点;没有数据返回 null。 ValueTask EarliestAsync(MetricLevel level, NodeId agentId, string series, CancellationToken ct); + /// 这条序列在某一层里最晚的窗口起点;没有数据返回 null。 + ValueTask LatestAsync(MetricLevel level, NodeId agentId, string series, CancellationToken ct); + ValueTask> ReadAgentsAsync(MetricLevel level, IReadOnlyCollection agentIds, string series, long fromMs, long toMs, CancellationToken ct); ValueTask DeleteOlderThanAsync(MetricLevel level, long cutoffMs, CancellationToken ct); diff --git a/backend/src/MoShiro.Backend.Application/Query/RowLoader.cs b/backend/src/MoShiro.Backend.Application/Query/RowLoader.cs index 56304b8..716777d 100644 --- a/backend/src/MoShiro.Backend.Application/Query/RowLoader.cs +++ b/backend/src/MoShiro.Backend.Application/Query/RowLoader.cs @@ -45,8 +45,19 @@ public sealed class RowLoader return at + 1000; } - IReadOnlyList rows = await _metrics.ReadAsync(MetricLevel.M1, agent, series, nowMs - (7 * Steps.Day), nowMs + 60_000, ct); - return rows.Count == 0 ? null : rows.Max(r => r.WindowStart) + 60_000; + // 热层空了(Backend 刚重启、热层还没攒起来)就看持久层:各层里最晚的窗口取最大。只看 1 分钟层会落后——它比 5 分钟层更新得慢时, + // 窗口就被定在好几个小时以前,页面显示的全是旧数据。 + long? latest = null; + foreach (MetricLevel level in new[] { MetricLevel.S10, MetricLevel.M1, MetricLevel.M5 }) + { + if (await _metrics.LatestAsync(level, agent, series, ct) is { } start) + { + long end = Math.Min(start + level.WidthMs(), nowMs); + latest = latest is null ? end : Math.Max(latest.Value, end); + } + } + + return latest; } /// diff --git a/backend/src/MoShiro.Backend.Storage.Sqlite/SqliteMetricStore.cs b/backend/src/MoShiro.Backend.Storage.Sqlite/SqliteMetricStore.cs index 155faf1..87d7b46 100644 --- a/backend/src/MoShiro.Backend.Storage.Sqlite/SqliteMetricStore.cs +++ b/backend/src/MoShiro.Backend.Storage.Sqlite/SqliteMetricStore.cs @@ -90,6 +90,11 @@ public sealed class SqliteMetricStore : IMetricStore c => c.Query($"SELECT MIN(window_start) FROM {level.TableName()} WHERE agent_id = @a AND series = @s", r => r.NullableLong(0), ("@a", agentId.Value), ("@s", series)).FirstOrDefault(), ct); + public ValueTask LatestAsync(MetricLevel level, NodeId agentId, string series, CancellationToken ct) => + _db.ReadAsync( + c => c.Query($"SELECT MAX(window_start) FROM {level.TableName()} WHERE agent_id = @a AND series = @s", r => r.NullableLong(0), ("@a", agentId.Value), ("@s", series)).FirstOrDefault(), + ct); + public ValueTask> ReadAgentsAsync(MetricLevel level, IReadOnlyCollection agentIds, string series, long fromMs, long toMs, CancellationToken ct) => _db.ReadAsync>( c =>