Skip to content

Commit

Permalink
fix: #1294: Streaming Genotype matcher response to prevent memory exc…
Browse files Browse the repository at this point in the history
…eptions.
  • Loading branch information
zabeen committed May 2, 2024
1 parent 00583ce commit f544488
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ public class MatchCalculationFunctions
MatchPredictionParameters = input.MatchPredictionParameters
});

var response = new GenotypeMatcherResponse
var matcherResponse = new GenotypeMatcherResponse
{
MatchPredictionParameters = input.MatchPredictionParameters,
PatientInfo = BuildSubjectResult(result.PatientResult, frequencySet.PatientSet, input.Patient),
DonorInfo = BuildSubjectResult(result.DonorResult, frequencySet.DonorSet, input.Donor),
MatchedGenotypePairs = result.GenotypeMatchDetails.ToSingleDelimitedString()
MatchedGenotypePairs = result.GenotypeMatchDetails.ToFormattedStrings(),
};

return new JsonResult(response);
return await StreamGenotypeMatcherResponse(request, matcherResponse);
}

private static SubjectResult BuildSubjectResult(
Expand All @@ -72,5 +72,41 @@ public class MatchCalculationFunctions
set,
subjectInfo.HlaTyping.ToPhenotypeInfo().PrettyPrint());
}

/// <summary>
/// <paramref name="matcherResponse"/> can be very large. Streaming to avoid memory issues.
/// </summary>
private static async Task<IActionResult> StreamGenotypeMatcherResponse(HttpRequest request, GenotypeMatcherResponse matcherResponse)
{
request.HttpContext.Response.ContentType = "application/json";
await using var writer = new StreamWriter(request.HttpContext.Response.Body);
await using var jsonWriter = new JsonTextWriter(writer);

await jsonWriter.WriteStartObjectAsync();

await jsonWriter.WritePropertyNameAsync($"{nameof(matcherResponse.MatchPredictionParameters)}");
await jsonWriter.WriteValueAsync(JsonConvert.SerializeObject(matcherResponse.MatchPredictionParameters));

await jsonWriter.WritePropertyNameAsync($"{nameof(matcherResponse.PatientInfo)}");
await jsonWriter.WriteValueAsync(JsonConvert.SerializeObject(matcherResponse.PatientInfo));

await jsonWriter.WritePropertyNameAsync($"{nameof(matcherResponse.DonorInfo)}");
await jsonWriter.WriteValueAsync(JsonConvert.SerializeObject(matcherResponse.DonorInfo));

await jsonWriter.WritePropertyNameAsync($"{nameof(matcherResponse.MatchedGenotypePairs)}");
await jsonWriter.WriteStartArrayAsync();
foreach (var pair in matcherResponse.MatchedGenotypePairs)
{
await jsonWriter.WriteValueAsync(JsonConvert.SerializeObject(pair));
}
await jsonWriter.WriteEndArrayAsync();

await jsonWriter.WriteEndObjectAsync();

await writer.FlushAsync();

// Required to avoid the host trying to add headers to the response
return new EmptyResult();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Atlas.MatchPrediction.ExternalInterface.Models.HaplotypeFrequencySet;
using System.Collections.Generic;
using Atlas.MatchPrediction.ExternalInterface.Models.HaplotypeFrequencySet;
using Atlas.MatchPrediction.Models;

namespace Atlas.MatchPrediction.Functions.Models.Debug
Expand All @@ -10,9 +11,9 @@ public class GenotypeMatcherResponse
public SubjectResult DonorInfo { get; set; }

/// <summary>
/// Patient-donor genotype pairs (represented as a single, formatted string) and their match counts.
/// Patient-donor genotype pairs and their match counts.
/// </summary>
public string MatchedGenotypePairs { get; set; }
public IEnumerable<string> MatchedGenotypePairs { get; set; }
}

public class SubjectResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,48 +24,35 @@ public static string ToSingleDelimitedString(this Dictionary<PhenotypeInfo<strin
return builder.ToString();
}

public static string ToSingleDelimitedString(this IEnumerable<GenotypeMatchDetails> genotypeMatchDetails)
/// <summary>
/// Converts a collection of GenotypeMatchDetails to a collection of formatted strings.
/// Uses yield return to avoid creating a large collection in memory.
/// </summary>
public static IEnumerable<string> ToFormattedStrings(this IEnumerable<GenotypeMatchDetails> genotypeMatchDetails)
{
// ReSharper disable once PossibleMultipleEnumeration - `IsNullOrEmpty` extension method does not enumerate full collection
if (genotypeMatchDetails.IsNullOrEmpty())
{
return "No available genotype match details.";
yield return "No available genotype match details.";
}

var builder = new StringBuilder();

var genotypePairs = BuildGenotypePairsWithMatchCounts(genotypeMatchDetails);
yield return $"Total{FieldDelimiter}" +
$"A{FieldDelimiter}" +
$"B{FieldDelimiter}" +
$"C{FieldDelimiter}" +
$"DQB1{FieldDelimiter}" +
$"DRB1{FieldDelimiter}" +
$"{BuildGenotypeLikelihoodHeader("P-")}{FieldDelimiter}" +
$"{BuildGenotypeLikelihoodHeader("D-")}";

foreach (var pair in genotypePairs)
// ReSharper disable once PossibleMultipleEnumeration
foreach (var details in genotypeMatchDetails.OrderByDescending(x => x.MatchCount))
{
builder.AppendLine(pair);
yield return $"{BuildCounts(details.MatchCount, details.MatchCounts)}{FieldDelimiter}" +
$"{details.PatientGenotype.ToDelimitedString(details.PatientGenotypeLikelihood)}{FieldDelimiter}" +
$"{details.DonorGenotype.ToDelimitedString(details.DonorGenotypeLikelihood)}";
}

return builder.ToString();
}

private static IEnumerable<string> BuildGenotypePairsWithMatchCounts(IEnumerable<GenotypeMatchDetails> genotypeMatchDetails)
{
var header = $"Total{FieldDelimiter}" +
$"A{FieldDelimiter}" +
$"B{FieldDelimiter}" +
$"C{FieldDelimiter}" +
$"DQB1{FieldDelimiter}" +
$"DRB1{FieldDelimiter}" +
$"{BuildGenotypeLikelihoodHeader("P-")}{FieldDelimiter}" +
$"{BuildGenotypeLikelihoodHeader("D-")}";

var formattedStrings = new List<string> { header };
formattedStrings.AddRange(
genotypeMatchDetails
.OrderByDescending(x => x.MatchCount)
.Select(x =>
$"{BuildCounts(x.MatchCount, x.MatchCounts)}{FieldDelimiter}" +
$"{x.PatientGenotype.ToDelimitedString(x.PatientGenotypeLikelihood)}{FieldDelimiter}" +
$"{x.DonorGenotype.ToDelimitedString(x.DonorGenotypeLikelihood)}"));

return formattedStrings;

string BuildCounts(int totalCount, LociInfo<int?> locusCounts) =>
$"{totalCount}{FieldDelimiter}" +
$"{locusCounts.A}{FieldDelimiter}" +
Expand Down
2 changes: 1 addition & 1 deletion Atlas.MatchPrediction/Models/GenotypeMatchDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class GenotypeMatchDetails
public decimal DonorGenotypeLikelihood { get; set; }
public LociInfo<int?> MatchCounts { get; set; }
public ISet<Locus> AvailableLoci { get; set; }
public int MatchCount => MatchCounts.Reduce((locus, value, accumulator) => accumulator + value ?? accumulator, 0);
public int MatchCount => MatchCounts.Reduce((_, value, accumulator) => accumulator + value ?? accumulator, 0);
public int MismatchCount => (AvailableLoci.Count * 2) - MatchCount;
}
}

0 comments on commit f544488

Please sign in to comment.