We're going to talk whether it's worth to migrate from ASP.NET Core 2.2 to 3.0 in terms of performance. In addition, we're raising topics about high-performance features like pipelines, span, and memory which are used in ASP.NET 3.0 by Microsoft to speed up the processing of requests.
11. 11
Hypothetical Use Case
Company needs to report all sale
transactions quaterly for statistics purposes.
Since the Company makes thousands of
transactions every day - report file is very
large.
12. 12
Hypothetical Use Case
Company needs to report all sale
transactions quaterly for statistics purposes.
Since the Company makes thousands of
transactions every day - report file is very
large.
Let’s assume the report is a CSV file.
13. 13
Hypothetical Use Case
Company needs to report all sale
transactions quaterly for statistics purposes.
Since the Company makes thousands of
transactions every day - report file is very
large.
Let’s assume the report is a CSV file.
15. 15
Naïve approach 2
// BETTER APPROACH
private async Task ProcessRequestAsync(Stream stream)
{
var buffer = new byte[1024];
while (true)
{
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
return; // EOF
}
var newLinePos = -1;
var bytesChecked = 0;
do
{
newLinePos = Array.IndexOf(buffer, (byte)'n', bytesChecked, bytesRead - bytesChecked);
if (newLinePos >= 0)
{
var lineLength = newLinePos - bytesChecked;
ProcessRowData(buffer, bytesChecked, lineLength);
}
bytesChecked += newLinePos + 1;
}
while (newLinePos > 0);
}
}
16. 16
CSV might be tricky
Some product; 5.0; "Amazingn
Category"; 10n
Other product; 1.0; Boring category; 2n
17. 17
CSV might be tricky
Some product; 5.0; "Amazingn
Category"; 10n
Other product; 1.0; Boring category; 2n
18. 18
How we can improve our approach?
We can allocate bigger buffer when we face longer line or multiline record
19. 19
How we can improve our approach?
We can allocate bigger buffer when we face longer line or multiline record
We can create own pool of buffers, new buffer would be created when the previous gets
filled up (i.e. in 80% or more).
21. 21
System.IO.Pipelines to the rescue
The mentioned issues can easily be solved by using Pipe.
Pipe
PipeWriter PipeReader
22. 22
Pipes
private async Task ProcessRequestAsync(Stream stream)
{
var pipe = new Pipe();
var readFileTask = ReadFileAsync(stream, pipe.Writer);
var processFileTask = ProcessFileAsync(pipe.Reader);
await Task.WhenAll(readFileTask, processFileTask);
}
23. 23
PipeWriter
private async Task ReadFileAsync(Stream stream, PipeWriter pipeWriter)
{
while (true)
{
Memory<byte> memory = pipeWriter.GetMemory(BufferSize);
int bytesRead = await stream.ReadAsync(memory);
if (bytesRead == 0)
{
break;
}
pipeWriter.Advance(bytesRead);
// flush data to PipeReader
FlushResult flushResult = await pipeWriter.FlushAsync();
if (flushResult.IsCompleted)
{
break;
}
}
pipeWriter.Complete(); // we are done
}
24. 24
PipeReader
private async Task ProcessFileAsync(PipeReader pipeReader)
{
while(true)
{
ReadResult result = await pipeReader.ReadAsync();
ReadOnlySequence<byte> buffer = result.Buffer;
SequencePosition? position = null;
do
{
position = buffer.PositionOf((byte)'n'); // find position of newline character, read multiline row…
if (position != null)
{
ProcessRowData(buffer.Slice(0, position.Value));
buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); // move to next line
}
}
while (position != null);
if (result.IsCompleted)
{
break;
}
pipeReader.AdvanceTo(buffer.Start, buffer.End); // let know the PipeReader how much bytes were consumed
}
pipeReader.Complete(); // we are done
}
25. 25
Partial read
Pipe.Reader.ReadAsync() Some product; 5
Some product; 5 .PositionOf((byte)'n') null
Pipe.Reader.ReadAsync() Some product; 5.0; Some Categoryn
Some product; 5.0; Some Categoryn .PositionOf((byte)'n') SequencePosition
Pipe.Reader.AdvanceTo( SequencePosition +1)
Pipe.Reader.ReadAsync() Other product; 3.0;…
39. 39
How is it relevant?
Pipes originally were introduced for internal use in Kestrel. Eventually, the’ve
evolved into a part of public API.
40. 40
How is it relevant?
Pipes originally were introduced for internal use in Kestrel. Eventually, the’ve
evolved into a part of public API.
The body of request in ASP.NET Core 3.0 is exposed via BodyReader property of
the HttpContext. BodyReader is in fact PipeReader.
41. 41
How is it relevant?
Pipes originally were introduced for internal use in Kestrel. Eventually, the’ve
evolved into a part of public API.
The body of request in ASP.NET Core 3.0 is exposed via BodyReader property of
the HttpContext. BodyReader is in fact PipeReader.
Response body can be written with BodyWriter (PipeWriter).
45. 45
Span<T> - Example
N A M E @ E M A I L
public ReadOnlySpan<char> GetName(ReadOnlySpan<char> email)
{
var @position = email.LastIndexOf(‘@’);
return @position == -1
? ReadOnlySpan<char>.Empty
: email.Slice(0, @position);
}
46. 46
Span<T> - Two versions
Background vector Created by brgfx - www.freepik.com
Tu myślałem wyjść od jakiegoś use-case'u. Np mamy jakiś duży plik CSV który chcemy sparsować. Prezentujemy stare podejście na streamach, płynnie przechodząc do System.IO.Pipelines i potem BodyReadera/Writera
Tu myślałem wyjść od jakiegoś use-case'u. Np mamy jakiś duży plik CSV który chcemy sparsować. Prezentujemy stare podejście na streamach, płynnie przechodząc do System.IO.Pipelines i potem BodyReadera/Writera
Tu myślałem wyjść od jakiegoś use-case'u. Np mamy jakiś duży plik CSV który chcemy sparsować. Prezentujemy stare podejście na streamach, płynnie przechodząc do System.IO.Pipelines i potem BodyReadera/Writera
KOD!
Możemy zaalokować większy bufor, jeżeli natrafimy na dłuższą linię lub rekord zawierający kilka linii. Przy okazji można stosować inne sztuczki, np. ArrayPool w celu minimalizowania liczby alokowanych buforów itp. Itd..
No ok. tylko wtedy przy zwiększaniu bufora kopiujemy pamięć. Dodatkowo warto by było potem zmniejszać ten bufor, żeby nie zajmować niepotrzebnie pamięci….pomysł nie do końca trafiony – generuje duże wykorzystanie pamięci.
Możemy poprzedni pomysł rozszerzyć o dodawanie dodatkowych buforów tylko wtedy, kiedy poprzedni zostanie wykorzystany. Potrzebujemy wtedy logiki do odczytywania fragmentów pojedynczego rekordu z wielu buforów, oznaczania zwolnionych już buforów (takich z których odczytaliśmy już dane rekordu) itp. Itd. Całość robi się bardzo skomplikowana…
A i tak nie jest do końca efektywnie, bo:
Odczyt danych ze strumienia jest uzależniony od szybkości parsowania.
Wykorzystanie zwykłej zarządzanej tablicy bajtów może mieć impact na GC – pinned memory, może prowadzić do fragmentacji pamięci.
Możemy zaalokować większy bufor, jeżeli natrafimy na dłuższą linię lub rekord zawierający kilka linii. Przy okazji można stosować inne sztuczki, np. ArrayPool w celu minimalizowania liczby alokowanych buforów itp. Itd..
No ok. tylko wtedy przy zwiększaniu bufora kopiujemy pamięć. Dodatkowo warto by było potem zmniejszać ten bufor, żeby nie zajmować niepotrzebnie pamięci….pomysł nie do końca trafiony – generuje duże wykorzystanie pamięci.
Możemy poprzedni pomysł rozszerzyć o dodawanie dodatkowych buforów tylko wtedy, kiedy poprzedni zostanie wykorzystany. Potrzebujemy wtedy logiki do odczytywania fragmentów pojedynczego rekordu z wielu buforów, oznaczania zwolnionych już buforów (takich z których odczytaliśmy już dane rekordu) itp. Itd. Całość robi się bardzo skomplikowana…
A i tak nie jest do końca efektywnie, bo:
Odczyt danych ze strumienia jest uzależniony od szybkości parsowania.
Wykorzystanie zwykłej zarządzanej tablicy bajtów może mieć impact na GC – pinned memory, może prowadzić do fragmentacji pamięci.
Pipe składa się z dwóch części:
PipeWriter – który zapisuje do naszej „rury” oraz PipeReader, który z tej „rury” czyta.
Mamy ładnie podzielony proces na 2 składowe – załadowanie danych ze strumienia oraz parsowanie tych danych.
Task.WhenAll wskazuje, że obie składowe będą wykonywały się asynchronicznie.
Pipe składa się z dwóch części:
PipeWriter – który zapisuje do naszej „rury” oraz PipeReader, który z tej „rury” czyta.
Mamy ładnie podzielony proces na 2 składowe – załadowanie danych ze strumienia oraz parsowanie tych danych.
Task.WhenAll wskazuje, że obie składowe będą wykonywały się asynchronicznie.
KOD
Jedną z największych zalet wykorzystanie Pipe’ów jest Partial Read – możemy w zasadzie pracować na danych bez ich „skonsumowania”.
Przykład ilustruje przykładowy parser HTTP.
Pod maską Pipe zarządza (linked) listą zaalokowanych buforów, które są wykorzystywane przez PipeWritera/PipeReadera. PipeReader.ReadAsync zwraca ReadOnlySequence<T>. Za jego pomocą możemy uzyskać dostęp do jednego lub więcej segmentów pamięci (ReadOnlyMemory<T>) – analogicznie do Span<T> i Memory<T> i stringów.
Pipe przechowuje informację o położeniu Writera i Readera w kontekście zaalokowanych danych z wykorzystaniem SequencePosition.
SequencePosition to nic innego jak wskaźnik na konkretne miejsce we wspomnianej linked liście.
ReadOnlySequence<T> i Sequence position to struktury.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
W idealnym przypadku to co przychodzi do serwera jest odczytywane przez jeden z wątków i jednocześnie parsowane i przetwarzane przez inny wątek. Samo parsowanie może jednak trwać znacznie dłużej niż ładowanie danych ze strumienia. W tej sytuacji wątek ładujący dane ze strumienia może albo alokować więcej pamięci, albo wstrzymać pracę na jakiś czas - należy znaleźć odpowiedni balans w celu zapewnienia optymalnej wydajności.
Do rozwiązania wspomnianego problemu Pipy zostały wyposażone w 2 właściwości – PauseWriterThreshold i ResumeWriterThreshold. Pierwsza definiuje ile danych powinno zostać zbuforowanych zanim wywołanie metody FlushAsync (PipeWriter) spowoduje wstrzymanie pracy Writera. Druga z kolei kontroluje ilość danych którą może zostać skonsumowanych przez PipeReadera zanim praca PipeWritera zostanie wznowiona.
Oczywiście w 99% przypadków wgl nie będziemy zajmowali się samodzielnym parsowaniem treści Body. Warto jednak wiedzieć jak to działa pod maską.
Oczywiście w 99% przypadków wgl nie będziemy zajmowali się samodzielnym parsowaniem treści Body. Warto jednak wiedzieć jak to działa pod maską.
Oczywiście w 99% przypadków wgl nie będziemy zajmowali się samodzielnym parsowaniem treści Body. Warto jednak wiedzieć jak to działa pod maską.
Oczywiście w 99% przypadków wgl nie będziemy zajmowali się samodzielnym parsowaniem treści Body. Warto jednak wiedzieć jak to działa pod maską.