Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Titanium.Inspector/Services/IInspectorPathPicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public Task<IReadOnlyList<string>> PickOpenPathsAsync(string title, string filte
internal readonly record struct StoragePickAttempt(bool DialogShown, string? Path)
{
public IReadOnlyList<string> Paths { get; init; } =
string.IsNullOrEmpty(Path) ? Array.Empty<string>() : [Path!];
string.IsNullOrEmpty(Path) ? Array.Empty<string>() : [Path];
}

internal static class InspectorPathPickerHelpers
Expand Down
72 changes: 43 additions & 29 deletions src/Titanium.Inspector/Services/SessionArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public static JsonObject ToHarEntry(SessionSnapshot s)
{
try
{
if (!TryReadHarRequest(entry, out var method, out var url, out var host, out var reqHeaders, out var reqBody, out var contentType, out var reqBytes))
if (!TryReadHarRequest(entry, out var request))
{
return null;
}
Expand All @@ -246,18 +246,18 @@ public static JsonObject ToHarEntry(SessionSnapshot s)
var snap = new SessionSnapshot
{
Id = id,
Method = method,
Url = url,
Host = host,
Method = request.Method,
Url = request.Url,
Host = request.Host,
StartedUtc = started,
StatusCode = status,
RequestHeadersText = reqHeaders,
RequestHeadersText = request.Headers,
ResponseHeadersText = respHeaders,
RequestBodyText = reqBody,
RequestBodyText = request.Body,
ResponseBodyText = respBody,
RequestBodyBytes = reqBytes,
RequestBodyBytes = request.BodyBytes,
ResponseBodyBytes = respBytes,
ContentType = contentType ?? respMime,
ContentType = request.ContentType ?? respMime,
DurationMs = durationMs,
TtfbMs = ttfbMs,
BodySize = respBytes?.LongLength ?? respBody?.Length,
Expand Down Expand Up @@ -386,6 +386,14 @@ private static void ApplyInspectorExtension(JsonElement entry, SessionSnapshot s
return;
}

ApplyInspectorIdentity(ext, snap);
ApplyInspectorCapture(ext, snap);
ApplyInspectorFlags(ext, snap);
ApplyInspectorBodies(ext, snap);
}

private static void ApplyInspectorIdentity(JsonElement ext, SessionSnapshot snap)
{
if (ext.TryGetProperty("id", out var idEl) && idEl.TryGetInt64(out var savedId) && savedId > 0)
{
snap.Id = savedId;
Expand Down Expand Up @@ -415,7 +423,10 @@ private static void ApplyInspectorExtension(JsonElement entry, SessionSnapshot s
{
snap.ProcessName = pn.GetString();
}
}

private static void ApplyInspectorCapture(JsonElement ext, SessionSnapshot snap)
{
if (ext.TryGetProperty("sentBytes", out var sent) && sent.TryGetInt64(out var sentBytes))
{
snap.SentBytes = sentBytes;
Expand Down Expand Up @@ -447,7 +458,10 @@ private static void ApplyInspectorExtension(JsonElement entry, SessionSnapshot s
{
snap.ResponseBodyOriginalSize = respOrig;
}
}

private static void ApplyInspectorFlags(JsonElement ext, SessionSnapshot snap)
{
snap.IsWebSocket = ReadBool(ext, "isWebSocket") ?? snap.IsWebSocket;
snap.IsGrpc = ReadBool(ext, "isGrpc") ?? snap.IsGrpc;
snap.IsTranscoded = ReadBool(ext, "isTranscoded") ?? snap.IsTranscoded;
Expand All @@ -468,7 +482,10 @@ private static void ApplyInspectorExtension(JsonElement entry, SessionSnapshot s
snap.UpstreamPath = ReadString(ext, "upstreamPath") ?? snap.UpstreamPath;
snap.UpstreamContentType = ReadString(ext, "upstreamContentType") ?? snap.UpstreamContentType;
snap.ProtobufDecodedText = ReadString(ext, "protobufDecodedText") ?? snap.ProtobufDecodedText;
}

private static void ApplyInspectorBodies(JsonElement ext, SessionSnapshot snap)
{
// Prefer _inspector body payloads when present (lossless for Inspect).
if (TryDecodeBase64(ext, "requestBodyBase64", out var reqBytes))
{
Expand Down Expand Up @@ -539,7 +556,7 @@ private static bool TryDecodeBase64(JsonElement ext, string name, out byte[] byt
}
}

private static IReadOnlyList<T>? DeserializeList<T>(JsonElement ext, string name)
private static List<T>? DeserializeList<T>(JsonElement ext, string name)
{
if (!ext.TryGetProperty(name, out var el) || el.ValueKind != JsonValueKind.Array)
{
Expand All @@ -556,37 +573,33 @@ private static bool TryDecodeBase64(JsonElement ext, string name, out byte[] byt
}
}

private static bool TryReadHarRequest(
JsonElement entry,
out string method,
out string url,
out string? host,
out string reqHeaders,
out string? reqBody,
out string? contentType,
out byte[]? reqBytes)
{
method = "GET";
url = "";
host = null;
reqHeaders = "";
reqBody = null;
contentType = null;
reqBytes = null;
private readonly record struct HarRequestFields(
string Method,
string Url,
string? Host,
string Headers,
string? Body,
string? ContentType,
byte[]? BodyBytes);

private static bool TryReadHarRequest(JsonElement entry, out HarRequestFields request)
{
request = new HarRequestFields("GET", "", null, "", null, null, null);
if (!entry.TryGetProperty("request", out var req))
{
return false;
}

method = req.TryGetProperty("method", out var m) ? m.GetString() ?? "GET" : "GET";
url = req.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
var method = req.TryGetProperty("method", out var m) ? m.GetString() ?? "GET" : "GET";
var url = req.TryGetProperty("url", out var u) ? u.GetString() ?? "" : "";
string? host = null;
if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
host = uri.Host;
}

reqHeaders = FormatHarHeaders(req);
string? reqBody = null;
string? contentType = null;
if (req.TryGetProperty("postData", out var post) && post.ValueKind == JsonValueKind.Object)
{
if (post.TryGetProperty("text", out var pt))
Expand All @@ -600,6 +613,7 @@ private static bool TryReadHarRequest(
}
}

request = new HarRequestFields(method, url, host, FormatHarHeaders(req), reqBody, contentType, null);
return true;
}

Expand Down
9 changes: 3 additions & 6 deletions src/Titanium.Inspector/Services/SessionBodyDiskCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ private void RemoveIndexEntryLocked(string path)
}
}

private void QueueCleanup(IReadOnlyList<string> paths)
private void QueueCleanup(List<string> paths)
{
if (paths.Count == 0)
{
Expand All @@ -704,12 +704,9 @@ private void QueueCleanup(IReadOnlyList<string> paths)

lock (_cleanupGate)
{
foreach (var path in paths)
foreach (var path in paths.Where(static path => !string.IsNullOrEmpty(path)))
{
if (!string.IsNullOrEmpty(path))
{
_cleanupPaths.Enqueue(path);
}
_cleanupPaths.Enqueue(path);
}

_cleanupTask = _cleanupTask.ContinueWith(
Expand Down
77 changes: 45 additions & 32 deletions src/Titanium.Inspector/Services/SessionStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,12 @@ public long? PinnedSessionId
{
var prevId = _pinnedSessionId;
_pinnedSessionId = value;
if (prevId is long oldId && oldId != value && _byId.TryGetValue(oldId, out previous))
// Drop RAM bodies for the previous selection when the file already exists.
if (prevId is long oldId && oldId != value && _byId.TryGetValue(oldId, out previous)
&& previous.BodiesOnDisk)
{
// Drop RAM bodies for the previous selection when the file already exists.
if (previous.BodiesOnDisk)
{
ClearBodyFields(previous);
RecalcInMemoryBodyBytesLocked();
}
ClearBodyFields(previous);
RecalcInMemoryBodyBytesLocked();
}

if (value is long newId && _byId.TryGetValue(newId, out next))
Expand Down Expand Up @@ -338,7 +336,8 @@ public async Task FlushDiskCleanupAsync(TimeSpan? timeout = null)
public async Task EnsureBodiesLoadedAsync(SessionSnapshot snapshot, CancellationToken ct = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_disk is null)
var disk = _disk;
if (disk is null)
{
return;
}
Expand All @@ -355,15 +354,43 @@ public async Task EnsureBodiesLoadedAsync(SessionSnapshot snapshot, Cancellation
}

// Fast path: file already gone and nothing pending — do not wait ~1s.
if (!_disk.FileExists(snapshot.Id) && Volatile.Read(ref _pendingSpills) == 0)
if (!disk.FileExists(snapshot.Id) && Volatile.Read(ref _pendingSpills) == 0)
{
snapshot.BodiesMissingFromDisk = true;
return;
}

// Under heavy capture, the spill writer may still be draining thousands of HARs.
// Keep waiting while work is queued; only mark missing once the channel is idle
// and the file is still absent (bounded by ct / ~2 minutes).
if (await WaitForSpilledBodyAsync(snapshot, disk, ct).ConfigureAwait(false))
{
return;
}

if (!HasInMemoryBodies(snapshot) &&
Volatile.Read(ref _pendingSpills) == 0 &&
!disk.FileExists(snapshot.Id))
{
snapshot.BodiesMissingFromDisk = true;
}
}

public async Task EnsureBodiesLoadedAsync(IEnumerable<SessionSnapshot> snapshots, CancellationToken ct = default)
{
foreach (var snap in snapshots)
{
ct.ThrowIfCancellationRequested();
await EnsureBodiesLoadedAsync(snap, ct).ConfigureAwait(false);
}
}

/// <summary>
/// Under heavy capture, the spill writer may still be draining thousands of HARs.
/// Keep waiting while work is queued; only mark missing once the channel is idle
/// and the file is still absent (bounded by ct / ~2 minutes).
/// Returns true when the caller should stop (body loaded or marked missing).
/// </summary>
private async Task<bool> WaitForSpilledBodyAsync(
SessionSnapshot snapshot, SessionBodyDiskCache disk, CancellationToken ct)
{
var deadline = DateTime.UtcNow + TimeSpan.FromMinutes(2);
while (DateTime.UtcNow < deadline)
{
Expand All @@ -373,42 +400,28 @@ public async Task EnsureBodiesLoadedAsync(SessionSnapshot snapshot, Cancellation
if (HasInMemoryBodies(snapshot))
{
snapshot.BodiesMissingFromDisk = false;
return;
return true;
}

if (_disk.TryLoad(snapshot))
if (disk.TryLoad(snapshot))
{
// Keep BodiesOnDisk=true so deselect can unload without rewriting the file.
snapshot.BodiesMissingFromDisk = false;
RecalcInMemoryBodyBytesLocked();
return;
return true;
}
}

if (Volatile.Read(ref _pendingSpills) == 0 && !_disk.FileExists(snapshot.Id))
if (Volatile.Read(ref _pendingSpills) == 0 && !disk.FileExists(snapshot.Id))
{
snapshot.BodiesMissingFromDisk = true;
return;
return true;
}

await Task.Delay(25, ct).ConfigureAwait(false);
}

if (!HasInMemoryBodies(snapshot) &&
Volatile.Read(ref _pendingSpills) == 0 &&
!_disk.FileExists(snapshot.Id))
{
snapshot.BodiesMissingFromDisk = true;
}
}

public async Task EnsureBodiesLoadedAsync(IEnumerable<SessionSnapshot> snapshots, CancellationToken ct = default)
{
foreach (var snap in snapshots)
{
ct.ThrowIfCancellationRequested();
await EnsureBodiesLoadedAsync(snap, ct).ConfigureAwait(false);
}
return false;
}

/// <summary>
Expand Down
Loading
Loading