Skip to content

Instantly share code, notes, and snippets.

@defufna
Created January 31, 2026 15:21
Show Gist options
  • Select an option

  • Save defufna/264317929df2562a3ba9e9bc5690c929 to your computer and use it in GitHub Desktop.

Select an option

Save defufna/264317929df2562a3ba9e9bc5690c929 to your computer and use it in GitHub Desktop.
Test disk speed script
#:property Configuration=Release
using System.Diagnostics;
using System.Text;
var sizes = new[] { 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 67108864, 134217728, 268435456 };
var configurations = new List<TestRun>();
long defaultTotalData = 256L * 1024 * 1024; // 1 GB
foreach (var size in sizes)
{
// Add default configuration: 1 worker, Buffered (WriteThrough=false)
configurations.Add(new TestRun
{
ChunkSize = size,
TotalFileSize = defaultTotalData,
NumWorkers = 1,
WriteThrough = false
});
}
foreach (var size in sizes)
{
// Add default configuration: 1 worker, Buffered (WriteThrough=false)
configurations.Add(new TestRun
{
ChunkSize = size,
TotalFileSize = defaultTotalData,
NumWorkers = 1,
WriteThrough = true
});
}
var html = new StringBuilder();
html.AppendLine("<html><head><script src='https://cdn.jsdelivr.net/npm/chart.js'></script></head><body>");
html.AppendLine("<h1>Disk Write Performance Test</h1>");
html.AppendLine("<div style='width: 800px;'><canvas id='perfChart'></canvas></div>");
html.AppendLine("<table border='1' style='margin-top: 20px; border-collapse: collapse; width: 800px; text-align: center;'>");
html.AppendLine("<tr><th>Chunk Size</th><th>Workers</th><th>Mode</th><th>Avg Time (s)</th><th>Avg Speed (MB/s)</th></tr>");
// Data structure for chart: SeriesName -> List of (ChunkSizeLabel, Speed)
// We assume we want to plot Speed vs ChunkSize for different configurations (Workers/Mode)
var chartData = new Dictionary<string, List<(string Label, double Speed)>>();
foreach (var config in configurations)
{
string seriesName = config.GetSeriesName();
if (!chartData.ContainsKey(seriesName))
{
chartData[seriesName] = new List<(string, double)>();
}
string sizeLabel = config.GetSizeLabel();
Console.WriteLine($"Testing: {sizeLabel}, {config.NumWorkers} Workers, WriteThrough={config.WriteThrough}...");
var runs = new List<(double Time, double Speed)>();
for (int run = 0; run < 5; run++)
{
// Setup
byte[] data = new byte[config.ChunkSize];
new Random().NextBytes(data);
long dataPerWorker = config.TotalFileSize / config.NumWorkers;
string filename = "test.bin";
// Preallocate file
FileOptions fileOptions = FileOptions.WriteThrough;
if (!config.WriteThrough) fileOptions = FileOptions.None;
using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
{
fs.SetLength(config.TotalFileSize);
}
// Measure
Stopwatch sw = Stopwatch.StartNew();
Parallel.For(0, config.NumWorkers, i =>
{
FileOptions workerOptions = config.WriteThrough ? FileOptions.WriteThrough : FileOptions.None;
// Each worker opens the same file
using var fs = new FileStream(filename, FileMode.Open, FileAccess.Write, FileShare.ReadWrite, 4096, workerOptions);
fs.Seek(i * dataPerWorker, SeekOrigin.Begin);
Stopwatch sw2 = Stopwatch.StartNew();
long written = 0;
while (written < dataPerWorker)
{
int toWrite = (int)Math.Min(config.ChunkSize, dataPerWorker - written);
fs.Write(data, 0, toWrite);
written += toWrite;
if(sw2.ElapsedMilliseconds > 5000)
{
Console.WriteLine($" Worker {i} written {written} bytes...");
sw2.Restart();
}
}
});
sw.Stop();
// Calculate
double sec = sw.Elapsed.TotalSeconds;
double mb = config.TotalFileSize / (1024.0 * 1024.0);
double speed = mb / sec;
runs.Add((sec, speed));
Console.Write(".");
// Cleanup
if (File.Exists(filename)) File.Delete(filename);
}
// Filter: Sort by Speed. Remove Lowest (slowest) and Highest (fastest).
runs.Sort((a, b) => a.Speed.CompareTo(b.Speed));
// Keep runs[1, 2, 3]
var validRuns = runs.Skip(1).Take(3).ToList();
double avgTime = validRuns.Average(r => r.Time);
double avgSpeed = validRuns.Average(r => r.Speed);
// Save for Chart
chartData[seriesName].Add((sizeLabel, avgSpeed));
Console.WriteLine($" Done. Avg: {avgSpeed:F2} MB/s");
string modeStr = config.WriteThrough ? "WriteThrough" : "Buffered";
html.AppendLine($"<tr><td>{sizeLabel}</td><td>{config.NumWorkers}</td><td>{modeStr}</td><td>{avgTime:F2}</td><td>{avgSpeed:F2}</td></tr>");
}
html.AppendLine("</table>");
// Generate Chart.js script
var allLabels = chartData.Values.SelectMany(v => v.Select(x => x.Label)).Distinct().ToList();
html.AppendLine("<script>");
html.AppendLine("const ctx = document.getElementById('perfChart').getContext('2d');");
html.AppendLine("new Chart(ctx, {");
html.AppendLine(" type: 'line',");
html.AppendLine(" data: {");
html.AppendLine($" labels: [{string.Join(",", allLabels.Select(s => $"'{s}'"))}],");
html.AppendLine(" datasets: [");
string[] colors = { "red", "blue", "green", "orange", "purple", "cyan", "magenta" };
int ci = 0;
foreach (var kvp in chartData)
{
var seriesName = kvp.Key;
var dataPoints = kvp.Value;
var speeds = dataPoints.Select(dp => dp.Speed).ToList();
html.AppendLine(" {");
html.AppendLine($" label: '{seriesName}',");
html.AppendLine($" data: [{string.Join(",", speeds.Select(v => v.ToString("F2")))}],");
html.AppendLine($" borderColor: '{colors[ci % colors.Length]}',");
html.AppendLine(" fill: false,");
html.AppendLine(" tension: 0.1");
html.AppendLine(" },");
ci++;
}
html.AppendLine(" ]");
html.AppendLine(" },");
html.AppendLine(" options: {");
html.AppendLine(" scales: {");
html.AppendLine(" y: { beginAtZero: true, title: { display: true, text: 'Speed (MB/s)' } },");
html.AppendLine(" x: { title: { display: true, text: 'Chunk Size' } }");
html.AppendLine(" }");
html.AppendLine(" }");
html.AppendLine("});");
html.AppendLine("</script>");
html.AppendLine("</body></html>");
File.WriteAllText("report.html", html.ToString());
Console.WriteLine("Report generated: report.html");
class TestRun
{
public int ChunkSize { get; set; }
public long TotalFileSize { get; set; }
public int NumWorkers { get; set; }
public bool WriteThrough { get; set; }
public string GetSeriesName()
{
string wt = WriteThrough ? "WriteThrough" : "Buffered";
return $"{NumWorkers} Workers ({wt})";
}
public string GetSizeLabel()
{
return ChunkSize < 1048576 ? $"{ChunkSize / 1024.0:F0} KB" : $"{ChunkSize / 1048576.0:F0} MB";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment