Phase 21 Direction B: bound network memory via Range-segmented forward fetch
Replace the open-ended forward GET with sequential bounded bytes=start-end segments, the next fetched only when the scheduler drains below low-water, so the browser holds ~one segment regardless of file size. Seek converges on the same loop. Strip BP-DIAG.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using DeepDrftModels.Enums;
|
||||
using DeepDrftPublic.Client.Clients;
|
||||
|
||||
namespace DeepDrftTests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins the Phase 21 Direction B bounded-Range request shape on <see cref="TrackMediaClient.GetTrackMedia"/>.
|
||||
/// The network-memory bound rests on each forward fetch being a finite <c>bytes=start-end</c> slice (so the
|
||||
/// browser buffers only one segment), and on the caller learning the file total from the 206
|
||||
/// <c>Content-Range</c> header (the EOF boundary the segment cursor advances toward). Both are request/response
|
||||
/// plumbing the harness can observe directly; the actual browser memory behaviour is Daniel's manual re-run.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class TrackMediaBoundedRangeTests
|
||||
{
|
||||
// Captures the outgoing Range header and returns a 206 with a Content-Range so TotalLength resolves.
|
||||
private sealed class RangeCapturingHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly long _total;
|
||||
public RangeHeaderValue? CapturedRange { get; private set; }
|
||||
|
||||
public RangeCapturingHandler(long total) => _total = total;
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
CapturedRange = request.Headers.Range;
|
||||
|
||||
var from = request.Headers.Range?.Ranges.First().From ?? 0;
|
||||
var to = request.Headers.Range?.Ranges.First().To ?? (_total - 1);
|
||||
var body = new byte[to - from + 1];
|
||||
|
||||
var response = new HttpResponseMessage(HttpStatusCode.PartialContent)
|
||||
{
|
||||
Content = new ByteArrayContent(body),
|
||||
};
|
||||
response.Content.Headers.ContentRange = new ContentRangeHeaderValue(from, to, _total);
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SingleClientFactory : IHttpClientFactory
|
||||
{
|
||||
private readonly HttpMessageHandler _handler;
|
||||
public SingleClientFactory(HttpMessageHandler handler) => _handler = handler;
|
||||
public HttpClient CreateClient(string name) =>
|
||||
new(_handler, disposeHandler: false) { BaseAddress = new Uri("https://content.test/") };
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetTrackMedia_WithByteEnd_SendsBoundedRange()
|
||||
{
|
||||
var handler = new RangeCapturingHandler(total: 100_000_000);
|
||||
var client = new TrackMediaClient(new SingleClientFactory(handler));
|
||||
|
||||
await client.GetTrackMedia("track-1", byteOffset: 0, byteEnd: 4 * 1024 * 1024 - 1, format: AudioFormat.Lossless);
|
||||
|
||||
var range = handler.CapturedRange!.Ranges.First();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(range.From, Is.EqualTo(0), "bounded request starts at the cursor");
|
||||
Assert.That(range.To, Is.EqualTo(4 * 1024 * 1024 - 1),
|
||||
"bounded request must carry an inclusive end so the server returns a finite slice (one segment)");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetTrackMedia_WithoutByteEnd_SendsOpenEndedRange()
|
||||
{
|
||||
var handler = new RangeCapturingHandler(total: 100_000_000);
|
||||
var client = new TrackMediaClient(new SingleClientFactory(handler));
|
||||
|
||||
await client.GetTrackMedia("track-1", byteOffset: 1024, byteEnd: null, format: AudioFormat.Lossless);
|
||||
|
||||
var range = handler.CapturedRange!.Ranges.First();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(range.From, Is.EqualTo(1024), "open-ended request starts at the cursor");
|
||||
Assert.That(range.To, Is.Null, "no byteEnd → open-ended bytes=start- (pre-Direction-B shape, kept working)");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task GetTrackMedia_206Response_SurfacesTotalLengthFromContentRange()
|
||||
{
|
||||
var handler = new RangeCapturingHandler(total: 970_000_000);
|
||||
var client = new TrackMediaClient(new SingleClientFactory(handler));
|
||||
|
||||
var result = await client.GetTrackMedia("track-1", byteOffset: 0, byteEnd: 4 * 1024 * 1024 - 1);
|
||||
|
||||
Assert.That(result.Success, Is.True);
|
||||
Assert.That(result.Value!.TotalLength, Is.EqualTo(970_000_000),
|
||||
"the file total (the segment cursor's EOF boundary) must come from the 206 Content-Range");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user