|
@@ -0,0 +1,163 @@
|
|
|
|
|
+// Licensed to the .NET Foundation under one or more agreements.
|
|
|
|
|
+// The .NET Foundation licenses this file to you under the MIT license.
|
|
|
|
|
+
|
|
|
|
|
+using System.Globalization;
|
|
|
|
|
+using System.Text;
|
|
|
|
|
+using BenchmarkDotNet.Attributes;
|
|
|
|
|
+using BenchmarkDotNet.Order;
|
|
|
|
|
+using static System.Net.Mime.MediaTypeNames;
|
|
|
|
|
+
|
|
|
|
|
+namespace Microsoft.AspNetCore.WebUtilities.Microbenchmarks;
|
|
|
|
|
+
|
|
|
|
|
+public class MultipartReaderBenchmark
|
|
|
|
|
+{
|
|
|
|
|
+ MemoryStream _stream;
|
|
|
|
|
+
|
|
|
|
|
+ [Params(6, 28, 70)]
|
|
|
|
|
+ public int BoundarySize { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ [Params(1, 2, 3)]
|
|
|
|
|
+ public int SectionCount { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ [Params(true, false)]
|
|
|
|
|
+ public bool LargePayload { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ private string Boundary { get; set; }
|
|
|
|
|
+
|
|
|
|
|
+ [GlobalSetup]
|
|
|
|
|
+ public void Setup()
|
|
|
|
|
+ {
|
|
|
|
|
+ string data;
|
|
|
|
|
+ switch (BoundarySize)
|
|
|
|
|
+ {
|
|
|
|
|
+ case 6:
|
|
|
|
|
+ Boundary = "abc123";
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 28:
|
|
|
|
|
+ Boundary = "9051914041544843365972754266";
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 70:
|
|
|
|
|
+ Boundary = "WbQvpJcaxJRjMqwLdioBSOyJk3fHYdo9hLCOSBkHYW70pU71tH3lJq6ZUcSErOl0NRn5uT";
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ throw new InvalidOperationException(nameof(BoundarySize));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch (SectionCount)
|
|
|
|
|
+ {
|
|
|
|
|
+ case 1:
|
|
|
|
|
+ data = $"--{Boundary}\r\n" +
|
|
|
|
|
+"Content-Disposition: form-data; name=\"text\"\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+$"text default{new string('a', LargePayload ? 10000000 : 0)}\r\n" +
|
|
|
|
|
+$"--{Boundary}--\r\n"; ;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 2:
|
|
|
|
|
+ data = $"--{Boundary}\r\n" +
|
|
|
|
|
+"Content-Disposition: form-data; name=\"text\"\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+"text default\r\n" +
|
|
|
|
|
+$"--{Boundary}\r\n" +
|
|
|
|
|
+"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" +
|
|
|
|
|
+"Content-Type: text/plain\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+$"Content of a.txt.{new string('a', LargePayload ? 10000000 : 0)}\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+$"--{Boundary}--\r\n";
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 3:
|
|
|
|
|
+ data = $"--{Boundary}\r\n" +
|
|
|
|
|
+"Content-Disposition: form-data; name=\"text\"\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+"text default\r\n" +
|
|
|
|
|
+$"--{Boundary}\r\n" +
|
|
|
|
|
+"Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\"\r\n" +
|
|
|
|
|
+"Content-Type: text/plain\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+$"Content of a.txt{new string('a', LargePayload ? 10000000 : 0)}\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+$"--{Boundary}\r\n" +
|
|
|
|
|
+"Content-Disposition: form-data; name=\"file2\"; filename=\"a.html\"\r\n" +
|
|
|
|
|
+"Content-Type: text/html\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+"<!DOCTYPE html><title>Content of a.html.</title>\r\n" +
|
|
|
|
|
+"\r\n" +
|
|
|
|
|
+$"--{Boundary}--\r\n";
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ throw new ArgumentOutOfRangeException(nameof(SectionCount));
|
|
|
|
|
+ }
|
|
|
|
|
+ _stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Benchmark]
|
|
|
|
|
+ public async Task MultipartReaderParsing()
|
|
|
|
|
+ {
|
|
|
|
|
+ var reader = new MultipartReader(Boundary, _stream);
|
|
|
|
|
+
|
|
|
|
|
+ var sectionCount = 0;
|
|
|
|
|
+ MultipartSection section;
|
|
|
|
|
+ do
|
|
|
|
|
+ {
|
|
|
|
|
+ section = await reader.ReadNextSectionAsync();
|
|
|
|
|
+ sectionCount += section is not null ? 1 : 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ while (section is not null);
|
|
|
|
|
+
|
|
|
|
|
+ _stream.Position = 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (sectionCount != SectionCount)
|
|
|
|
|
+ {
|
|
|
|
|
+ throw new InvalidOperationException();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ [Benchmark]
|
|
|
|
|
+ public async Task MultipartReaderParsingWithRead()
|
|
|
|
|
+ {
|
|
|
|
|
+ var reader = new MultipartReader(Boundary, _stream);
|
|
|
|
|
+
|
|
|
|
|
+ var sectionCount = 0;
|
|
|
|
|
+ MultipartSection section;
|
|
|
|
|
+ do
|
|
|
|
|
+ {
|
|
|
|
|
+ section = await reader.ReadNextSectionAsync();
|
|
|
|
|
+ sectionCount += section is not null ? 1 : 0;
|
|
|
|
|
+
|
|
|
|
|
+ section?.Body.CopyTo(NullStream.Instance);
|
|
|
|
|
+ }
|
|
|
|
|
+ while (section is not null);
|
|
|
|
|
+
|
|
|
|
|
+ _stream.Position = 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (sectionCount != SectionCount)
|
|
|
|
|
+ {
|
|
|
|
|
+ throw new InvalidOperationException();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public class NullStream : Stream
|
|
|
|
|
+ {
|
|
|
|
|
+ public static readonly NullStream Instance = new NullStream();
|
|
|
|
|
+
|
|
|
|
|
+ public override bool CanRead => true;
|
|
|
|
|
+
|
|
|
|
|
+ public override bool CanSeek => true;
|
|
|
|
|
+
|
|
|
|
|
+ public override bool CanWrite => true;
|
|
|
|
|
+
|
|
|
|
|
+ public override long Length => 0;
|
|
|
|
|
+
|
|
|
|
|
+ public override long Position { get => 0; set { } }
|
|
|
|
|
+
|
|
|
|
|
+ public override void Flush() { }
|
|
|
|
|
+
|
|
|
|
|
+ public override int Read(byte[] buffer, int offset, int count) => 0;
|
|
|
|
|
+
|
|
|
|
|
+ public override long Seek(long offset, SeekOrigin origin) => 0;
|
|
|
|
|
+
|
|
|
|
|
+ public override void SetLength(long value) { }
|
|
|
|
|
+
|
|
|
|
|
+ public override void Write(byte[] buffer, int offset, int count) { }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|