Skip to content

Commit 410ad1a

Browse files
committed
- RESP3
- don't expose raw arrays - expose API-shaped ms/us accessors - reuse shared all-slots array
1 parent f41f9ff commit 410ad1a

5 files changed

Lines changed: 97 additions & 46 deletions

File tree

src/StackExchange.Redis/ClusterConfiguration.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ private SlotRange(short from, short to)
4545
/// </summary>
4646
public int To => to;
4747

48+
internal const int MinSlot = 0, MaxSlot = 16383;
49+
internal static readonly SlotRange[] SharedAllSlots = [new(MinSlot, MaxSlot)];
50+
4851
/// <summary>
4952
/// Indicates whether two ranges are not equal.
5053
/// </summary>

src/StackExchange.Redis/HotKeys.ResultProcessor.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private HotKeysResult(in RawResult result)
5353
if (len == 0) continue;
5454

5555
var items = value.GetItems().GetEnumerator();
56-
var slots = new SlotRange[len];
56+
var slots = len == 1 ? null : new SlotRange[len];
5757
for (int i = 0; i < len && items.MoveNext(); i++)
5858
{
5959
ref readonly RawResult pair = ref items.Current;
@@ -62,13 +62,21 @@ private HotKeysResult(in RawResult result)
6262
&& pair[0].TryGetInt64(out var from)
6363
&& pair[1].TryGetInt64(out var to))
6464
{
65-
slots[i] = new((int)from, (int)to);
65+
if (len == 1 & from == SlotRange.MinSlot & to == SlotRange.MaxSlot)
66+
{
67+
slots = SlotRange.SharedAllSlots; // avoid the alloc
68+
}
69+
else
70+
{
71+
slots ??= new SlotRange[len];
72+
slots[i] = new((int)from, (int)to);
73+
}
6674
}
6775
}
68-
SelectedSlots = slots;
76+
_selectedSlots = slots;
6977
break;
7078
case all_commands_all_slots_us.Hash when all_commands_all_slots_us.Is(hash, key) && value.TryGetInt64(out var i64):
71-
TotalCpuTimeMilliseconds = i64;
79+
TotalCpuTimeMicroseconds = i64;
7280
break;
7381
case net_bytes_all_commands_all_slots.Hash when net_bytes_all_commands_all_slots.Is(hash, key) && value.TryGetInt64(out var i64):
7482
TotalNetworkBytes = i64;
@@ -103,7 +111,7 @@ private HotKeysResult(in RawResult result)
103111
}
104112
}
105113

106-
CpuByKey = cpuTime;
114+
_cpuByKey = cpuTime;
107115
break;
108116
case by_net_bytes.Hash when by_net_bytes.Is(hash, key) & value.Resp2TypeArray is ResultType.Array:
109117
len = value.ItemsCount / 2;
@@ -120,7 +128,7 @@ private HotKeysResult(in RawResult result)
120128
}
121129
}
122130

123-
NetworkBytesByKey = netBytes;
131+
_networkBytesByKey = netBytes;
124132
break;
125133
}
126134
}

src/StackExchange.Redis/HotKeys.cs

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -124,47 +124,73 @@ public sealed partial class HotKeysResult
124124
/// <summary>
125125
/// The key slots active for this profiling session.
126126
/// </summary>
127-
public SlotRange[] SelectedSlots { get; } = [];
127+
public ReadOnlySpan<SlotRange> SelectedSlots => _selectedSlots;
128+
129+
private readonly SlotRange[]? _selectedSlots;
128130

129131
/// <summary>
130132
/// The total CPU measured for all commands in all slots.
131133
/// </summary>
132-
public TimeSpan TotalCpuTime => TimeSpan.FromMilliseconds(TotalCpuTimeMilliseconds);
134+
public TimeSpan TotalCpuTime => NonNegativeMicroseconds(TotalCpuTimeMicroseconds);
135+
136+
private static TimeSpan NonNegativeMilliseconds(long ms)
137+
=> TimeSpan.FromMilliseconds(Math.Max(ms, 0));
133138

134-
private long TotalCpuTimeMilliseconds { get; }
139+
private static TimeSpan NonNegativeMicroseconds(long us)
140+
{
141+
const long TICKS_PER_MICROSECOND = TimeSpan.TicksPerMillisecond / 1000; // 10, but: clearer
142+
return TimeSpan.FromTicks(Math.Max(us, 0) / TICKS_PER_MICROSECOND);
143+
}
144+
145+
/// <summary>
146+
/// The total CPU measured for all commands in all slots.
147+
/// </summary>
148+
public long TotalCpuTimeMicroseconds { get; } = -1;
135149

136150
/// <summary>
137151
/// The total network usage measured for all commands in all slots.
138152
/// </summary>
139153
public long TotalNetworkBytes { get; }
140154

141-
private long CollectionStartTimeUnixMilliseconds { get; }
155+
/// <summary>
156+
/// The start time of the capture.
157+
/// </summary>
158+
public long CollectionStartTimeUnixMilliseconds { get; } = -1;
142159

143160
/// <summary>
144161
/// The start time of the capture.
145162
/// </summary>
146-
public DateTime CollectionStartTime => RedisBase.UnixEpoch.AddMilliseconds(CollectionStartTimeUnixMilliseconds);
163+
public DateTime CollectionStartTime => RedisBase.UnixEpoch.AddMilliseconds(Math.Max(CollectionStartTimeUnixMilliseconds, 0));
147164

148-
private long CollectionDurationMilliseconds { get; }
165+
/// <summary>
166+
/// The duration of the capture.
167+
/// </summary>
168+
public long CollectionDurationMilliseconds { get; }
149169

150170
/// <summary>
151171
/// The duration of the capture.
152172
/// </summary>
153-
public TimeSpan CollectionDuration => TimeSpan.FromMilliseconds(CollectionDurationMilliseconds);
173+
public TimeSpan CollectionDuration => NonNegativeMilliseconds(CollectionDurationMilliseconds);
154174

155-
private long TotalCpuTimeUserMilliseconds { get; }
175+
/// <summary>
176+
/// The total user CPU time measured.
177+
/// </summary>
178+
public long TotalCpuTimeUserMilliseconds { get; } = -1;
156179

157180
/// <summary>
158181
/// The total user CPU time measured.
159182
/// </summary>
160-
public TimeSpan TotalCpuTimeUser => TimeSpan.FromMilliseconds(TotalCpuTimeUserMilliseconds);
183+
public TimeSpan TotalCpuTimeUser => NonNegativeMilliseconds(TotalCpuTimeUserMilliseconds);
161184

162-
private long TotalCpuTimeSystemMilliseconds { get; }
185+
/// <summary>
186+
/// The total system CPU measured.
187+
/// </summary>
188+
public long TotalCpuTimeSystemMilliseconds { get; } = -1;
163189

164190
/// <summary>
165191
/// The total system CPU measured.
166192
/// </summary>
167-
public TimeSpan TotalCpuTimeSystem => TimeSpan.FromMilliseconds(TotalCpuTimeSystemMilliseconds);
193+
public TimeSpan TotalCpuTimeSystem => NonNegativeMilliseconds(TotalCpuTimeSystemMilliseconds);
168194

169195
/// <summary>
170196
/// The total network data measured.
@@ -178,21 +204,23 @@ public sealed partial class HotKeysResult
178204
/// <summary>
179205
/// Hot keys, as measured by CPU activity.
180206
/// </summary>
181-
public MetricKeyCpu[] CpuByKey { get; } = [];
207+
public ReadOnlySpan<MetricKeyCpu> CpuByKey => _cpuByKey;
208+
209+
private readonly MetricKeyCpu[]? _cpuByKey;
182210

183211
/// <summary>
184212
/// Hot keys, as measured by network activity.
185213
/// </summary>
186-
public MetricKeyBytes[] NetworkBytesByKey { get; } = [];
214+
public ReadOnlySpan<MetricKeyBytes> NetworkBytesByKey => _networkBytesByKey;
187215

188-
private const long TicksPerMicroSeconds = TimeSpan.TicksPerMillisecond / 1000; // 10, but: clearer
216+
private readonly MetricKeyBytes[]? _networkBytesByKey;
189217

190218
/// <summary>
191219
/// A hot key, as measured by CPU activity.
192220
/// </summary>
193221
/// <param name="key">The key observed.</param>
194-
/// <param name="microSeconds">The time taken, in microseconds.</param>
195-
public readonly struct MetricKeyCpu(in RedisKey key, long microSeconds)
222+
/// <param name="durationMicroseconds">The time taken, in microseconds.</param>
223+
public readonly struct MetricKeyCpu(in RedisKey key, long durationMicroseconds)
196224
{
197225
private readonly RedisKey _key = key;
198226

@@ -204,22 +232,22 @@ public readonly struct MetricKeyCpu(in RedisKey key, long microSeconds)
204232
/// <summary>
205233
/// The time taken, in microseconds.
206234
/// </summary>
207-
public long MicroSeconds => microSeconds;
235+
public long DurationMicroseconds => durationMicroseconds;
208236

209237
/// <summary>
210238
/// The time taken.
211239
/// </summary>
212-
public TimeSpan Duration => TimeSpan.FromTicks(microSeconds / TicksPerMicroSeconds);
240+
public TimeSpan Duration => NonNegativeMicroseconds(durationMicroseconds);
213241

214242
/// <inheritdoc/>
215243
public override string ToString() => $"{_key}: {Duration}";
216244

217245
/// <inheritdoc/>
218-
public override int GetHashCode() => _key.GetHashCode() ^ microSeconds.GetHashCode();
246+
public override int GetHashCode() => _key.GetHashCode() ^ durationMicroseconds.GetHashCode();
219247

220248
/// <inheritdoc/>
221249
public override bool Equals(object? obj)
222-
=> obj is MetricKeyCpu other && _key.Equals(other.Key) && MicroSeconds == other.MicroSeconds;
250+
=> obj is MetricKeyCpu other && _key.Equals(other.Key) && durationMicroseconds == DurationMicroseconds;
223251
}
224252

225253
/// <summary>

src/StackExchange.Redis/PublicAPI/PublicAPI.Unshipped.txt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,30 @@
44
[SER003]StackExchange.Redis.HotKeysMetrics.Network = 2 -> StackExchange.Redis.HotKeysMetrics
55
[SER003]StackExchange.Redis.HotKeysResult
66
[SER003]StackExchange.Redis.HotKeysResult.CollectionDuration.get -> System.TimeSpan
7+
[SER003]StackExchange.Redis.HotKeysResult.CollectionDurationMilliseconds.get -> long
78
[SER003]StackExchange.Redis.HotKeysResult.CollectionStartTime.get -> System.DateTime
8-
[SER003]StackExchange.Redis.HotKeysResult.CpuByKey.get -> StackExchange.Redis.HotKeysResult.MetricKeyCpu[]!
9+
[SER003]StackExchange.Redis.HotKeysResult.CollectionStartTimeUnixMilliseconds.get -> long
10+
[SER003]StackExchange.Redis.HotKeysResult.CpuByKey.get -> System.ReadOnlySpan<StackExchange.Redis.HotKeysResult.MetricKeyCpu>
911
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes
1012
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.Bytes.get -> long
1113
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.Key.get -> StackExchange.Redis.RedisKey
1214
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.MetricKeyBytes() -> void
1315
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyBytes.MetricKeyBytes(in StackExchange.Redis.RedisKey key, long bytes) -> void
1416
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu
1517
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.Duration.get -> System.TimeSpan
18+
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.DurationMicroseconds.get -> long
1619
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.Key.get -> StackExchange.Redis.RedisKey
1720
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu() -> void
18-
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu(in StackExchange.Redis.RedisKey key, long microSeconds) -> void
19-
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MicroSeconds.get -> long
20-
[SER003]StackExchange.Redis.HotKeysResult.NetworkBytesByKey.get -> StackExchange.Redis.HotKeysResult.MetricKeyBytes[]!
21+
[SER003]StackExchange.Redis.HotKeysResult.MetricKeyCpu.MetricKeyCpu(in StackExchange.Redis.RedisKey key, long durationMicroseconds) -> void
22+
[SER003]StackExchange.Redis.HotKeysResult.NetworkBytesByKey.get -> System.ReadOnlySpan<StackExchange.Redis.HotKeysResult.MetricKeyBytes>
2123
[SER003]StackExchange.Redis.HotKeysResult.SampleRatio.get -> long
22-
[SER003]StackExchange.Redis.HotKeysResult.SelectedSlots.get -> StackExchange.Redis.SlotRange[]!
24+
[SER003]StackExchange.Redis.HotKeysResult.SelectedSlots.get -> System.ReadOnlySpan<StackExchange.Redis.SlotRange>
2325
[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTime.get -> System.TimeSpan
26+
[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeMicroseconds.get -> long
2427
[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeSystem.get -> System.TimeSpan
28+
[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeSystemMilliseconds.get -> long
2529
[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeUser.get -> System.TimeSpan
30+
[SER003]StackExchange.Redis.HotKeysResult.TotalCpuTimeUserMilliseconds.get -> long
2631
[SER003]StackExchange.Redis.HotKeysResult.TotalNetworkBytes.get -> long
2732
[SER003]StackExchange.Redis.HotKeysResult.TotalNetworkBytes2.get -> long
2833
[SER003]StackExchange.Redis.HotKeysResult.TrackingActive.get -> bool

tests/StackExchange.Redis.Tests/HotKeysTests.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace StackExchange.Redis.Tests;
66

7+
[RunPerProtocol]
78
[Collection(NonParallelCollection.Name)]
89
public class HotKeysTests(ITestOutputHelper output, SharedConnectionFixture fixture) : TestBase(output, fixture)
910
{
@@ -78,25 +79,31 @@ public void CanStartStopReset()
7879

7980
private static void CheckSimpleWithKey(RedisKey key, HotKeysResult hotKeys)
8081
{
81-
Assert.True(hotKeys.CollectionDuration > TimeSpan.Zero);
82-
Assert.True(hotKeys.CollectionStartTime > new DateTime(2026, 2, 1));
83-
var cpu = Assert.Single(hotKeys.CpuByKey);
82+
Assert.True(hotKeys.CollectionDurationMilliseconds >= 0, nameof(hotKeys.CollectionDurationMilliseconds));
83+
Assert.True(hotKeys.CollectionStartTimeUnixMilliseconds >= 0, nameof(hotKeys.CollectionStartTimeUnixMilliseconds));
84+
85+
Assert.Equal(1, hotKeys.CpuByKey.Length);
86+
var cpu = hotKeys.CpuByKey[0];
8487
Assert.Equal(key, cpu.Key);
85-
Assert.True(cpu.Duration > TimeSpan.Zero);
86-
var net = Assert.Single(hotKeys.NetworkBytesByKey);
88+
Assert.True(cpu.DurationMicroseconds >= 0, nameof(cpu.DurationMicroseconds));
89+
90+
Assert.Equal(1, hotKeys.NetworkBytesByKey.Length);
91+
var net = hotKeys.NetworkBytesByKey[0];
8792
Assert.Equal(key, net.Key);
88-
Assert.True(net.Bytes > 0);
93+
Assert.True(net.Bytes > 0, nameof(net.Bytes));
8994

9095
Assert.Equal(1, hotKeys.SampleRatio);
91-
var slots = Assert.Single(hotKeys.SelectedSlots);
92-
Assert.Equal(0, slots.From);
93-
Assert.Equal(16383, slots.To);
94-
95-
Assert.True(hotKeys.TotalCpuTime > TimeSpan.Zero);
96-
Assert.True(hotKeys.TotalCpuTimeSystem >= TimeSpan.Zero);
97-
Assert.True(hotKeys.TotalCpuTimeUser >= TimeSpan.Zero);
98-
Assert.True(hotKeys.TotalNetworkBytes > 0);
99-
Assert.True(hotKeys.TotalNetworkBytes2 > 0);
96+
97+
Assert.Equal(1, hotKeys.SelectedSlots.Length);
98+
var slots = hotKeys.SelectedSlots[0];
99+
Assert.Equal(SlotRange.MinSlot, slots.From);
100+
Assert.Equal(SlotRange.MaxSlot, slots.To);
101+
102+
Assert.True(hotKeys.TotalCpuTimeMicroseconds >= 0, nameof(hotKeys.TotalCpuTimeMicroseconds));
103+
Assert.True(hotKeys.TotalCpuTimeSystemMilliseconds >= 0, nameof(hotKeys.TotalCpuTimeSystemMilliseconds));
104+
Assert.True(hotKeys.TotalCpuTimeUserMilliseconds >= 0, nameof(hotKeys.TotalCpuTimeUserMilliseconds));
105+
Assert.True(hotKeys.TotalNetworkBytes > 0, nameof(hotKeys.TotalNetworkBytes));
106+
Assert.True(hotKeys.TotalNetworkBytes2 > 0, nameof(hotKeys.TotalNetworkBytes2));
100107
}
101108

102109
[Fact]

0 commit comments

Comments
 (0)