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
15 changes: 11 additions & 4 deletions src/SQLQueryStress/LoadEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,14 @@ public static bool ExecuteCommand(string connectionString, string sql)
return true;
}

public void StartLoad(BackgroundWorker worker, int queryDelay)
public void StartLoad(BackgroundWorker worker, int queryDelay, Action<QueryOutput> outputCallback = null)
{
_queryDelay = queryDelay;

StartLoad(worker);
StartLoad(worker, outputCallback);
}

private void StartLoad(BackgroundWorker worker)
private void StartLoad(BackgroundWorker worker, Action<QueryOutput> outputCallback)
{
var useParams = false;

Expand Down Expand Up @@ -203,7 +203,14 @@ private void StartLoad(BackgroundWorker worker)
//Report output to the UI
int finishedThreads = Interlocked.CompareExchange(ref _finishedThreads, 0, 0);
theOut.ActiveThreads = _threads - finishedThreads;
worker.ReportProgress((int)(_finishedThreads / (decimal)_threads * 100), theOut);
if (outputCallback == null)
{
worker.ReportProgress((int)(_finishedThreads / (decimal)_threads * 100), theOut);
}
else
{
outputCallback(theOut);
}
}
}
}
Expand Down
46 changes: 46 additions & 0 deletions src/SQLQueryStress/RunResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.Runtime.Serialization;

namespace SQLQueryStress
{
[Serializable]
[DataContract]
public sealed record RunResult
{
[DataMember(Order = 1)]
public Guid TestId { get; init; }

[DataMember(Order = 2)]
public string StartTime { get; init; } = string.Empty;

[DataMember(Order = 3)]
public double ElapsedTime { get; init; }

[DataMember(Order = 4)]
public int Threads { get; init; }

[DataMember(Order = 5)]
public int Iterations { get; init; }

[DataMember(Order = 6)]
public int CompletedIterations { get; init; }

[DataMember(Order = 7)]
public int Delay { get; init; }

[DataMember(Order = 8)]
public double AvgCpuSeconds { get; init; }

[DataMember(Order = 9)]
public double AvgActualSeconds { get; init; }

[DataMember(Order = 10)]
public double AvgClientSeconds { get; init; }

[DataMember(Order = 11)]
public double AvgLogicalReads { get; init; }

[DataMember(Order = 12)]
public int ExceptionCount { get; init; }
}
}
7 changes: 5 additions & 2 deletions src/SqlQueryStressCLI/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ public class CommandLineOptions



//TODO Implement
[Option('r', "results",
HelpText = "Autosave results to specified file")]
HelpText = "Autosave results to the specified file (CSV or JSON)")]
public string ResultsAutoSaveFileName { get; set; } = string.Empty;

[Option('f', "format",
HelpText = "Result output format: csv or json (defaults to file extension)")]
public string ResultFormat { get; set; } = string.Empty;
}
}
137 changes: 108 additions & 29 deletions src/SqlQueryStressCLI/LoadRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,31 @@ public class LoadRunner
private System.Timers.Timer timer = new System.Timers.Timer(100);

private readonly CommandLineOptions _runParameters;
private readonly ManualResetEventSlim _runCompleted = new ManualResetEventSlim();
private bool _runSucceeded = true;

public LoadRunner(QueryStressSettings settings, CommandLineOptions options)
{
_settings = settings;
_runParameters = options;
}

public void Run()
public bool Run()
{
if (!string.IsNullOrEmpty(_runParameters.ResultsAutoSaveFileName))
{
var format = GetResultFormat(_runParameters.ResultsAutoSaveFileName);
if (format != "csv" && format != "json")
{
Console.Error.WriteLine($"Unsupported result format '{format}'. Use csv or json.");
return false;
}
}

if (!_settings.MainDbConnectionInfo.TestConnection())
{
Console.Error.WriteLine("Invalid connection info");
return;
return false;
}

_testStartTime = DateTime.Now;
Expand Down Expand Up @@ -124,10 +136,8 @@ public void Run()
_settings.ParamQuery, _settings.ParamMappings, paramConnectionInfo.ConnectionString, _settings.CommandTimeout, _settings.CollectIoStats,
_settings.CollectTimeStats, _settings.ForceDataRetrieval, _settings.KillQueriesOnCancel, _backgroundWorkerCTS, _settings.ParamTypeMappings);

backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
backgroundWorker1.DoWork += new DoWorkEventHandler(this.backgroundWorker1_DoWork);
backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(this.backgroundWorker1_ProgressChanged);
backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);

backgroundWorker1.RunWorkerAsync(engine);
Expand All @@ -137,13 +147,9 @@ public void Run()
//timer.Enabled = true;

_start = new TimeSpan(DateTime.Now.Ticks);

while (backgroundWorker1.IsBusy)
{
Thread.Sleep(1000);
}

Thread.Sleep(2000);
_runCompleted.Wait();
var succeeded = _runSucceeded;
return succeeded;
}

private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
Expand All @@ -153,13 +159,11 @@ private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
((LoadEngine)e.Argument).StartLoad(backgroundWorker1, _settings.DelayBetweenQueries);
((LoadEngine)e.Argument).StartLoad(backgroundWorker1, _settings.DelayBetweenQueries, ProcessQueryOutput);
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
private void ProcessQueryOutput(LoadEngine.QueryOutput output)
{
var output = (LoadEngine.QueryOutput)e.UserState;

_totalIterations++;

if (output.LogicalReads > 0)
Expand Down Expand Up @@ -218,14 +222,29 @@ private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEve

private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
UpdateUi();
try
{
UpdateUi();

if (e.Error != null)
{
Console.Error.WriteLine($"Load failed: {e.Error.Message}");
_runSucceeded = false;
return;
}

((BackgroundWorker)sender).Dispose();
_backgroundWorkerCTS?.Dispose();
_runSucceeded = _totalExceptions == 0;

if (!string.IsNullOrEmpty(_runParameters.ResultsAutoSaveFileName))
if (!string.IsNullOrEmpty(_runParameters.ResultsAutoSaveFileName))
{
_runSucceeded = AutoSaveResults(_runParameters.ResultsAutoSaveFileName) && _runSucceeded;
Comment thread
ErikEJ marked this conversation as resolved.
}
}
finally
{
AutoSaveResults(_runParameters.ResultsAutoSaveFileName);
((BackgroundWorker)sender).Dispose();
_backgroundWorkerCTS?.Dispose();
_runCompleted.Set();
}
}

Expand Down Expand Up @@ -268,16 +287,75 @@ private void UpdateUi()
AnsiConsole.Write(table);
}

private void AutoSaveResults(string resultsAutoSaveFileName)
private bool AutoSaveResults(string resultsAutoSaveFileName)
{
string extension = Path.GetExtension(resultsAutoSaveFileName).ToUpperInvariant();
if (extension.Equals(".csv", StringComparison.InvariantCultureIgnoreCase))
var format = GetResultFormat(resultsAutoSaveFileName);

switch (format)
{
case "csv":
return ExportBenchMarkToCsvFile(resultsAutoSaveFileName);
case "json":
return ExportBenchmarkToJsonFile(resultsAutoSaveFileName);
default:
Console.Error.WriteLine($"Unsupported result format '{format}'. Use csv or json.");
return false;
}
}

private string GetResultFormat(string fileName)
{
if (!string.IsNullOrWhiteSpace(_runParameters.ResultFormat))
{
ExportBenchMarkToCsvFile(resultsAutoSaveFileName);
var format = _runParameters.ResultFormat.Trim();
return format.ToLowerInvariant();
}

return Path.GetExtension(fileName).TrimStart('.').ToLowerInvariant();
}

private bool ExportBenchmarkToJsonFile(string fileName)
{
try
{
var result = BuildRunResult();
var json = JsonSerializer.WriteFromObject(result);
File.WriteAllText(fileName, json, Encoding.UTF8);
return true;
}
catch (Exception)
{
Console.Error.WriteLine($"There was an error saving the benchmark to '{fileName}', make sure you have write privileges to that path");
return false;
}
}

private RunResult BuildRunResult()
{
var elapsed = _testStartTime == default ? 0d : (DateTime.Now - _testStartTime).TotalSeconds;
var avgClientSeconds = _totalIterations == 0 ? 0d : _totalTime / _totalIterations / 1000d;
var avgCpuSeconds = _totalTimeMessages == 0 ? 0d : _totalCpuTime / _totalTimeMessages / 1000d;
var avgActualSeconds = _totalTimeMessages == 0 ? 0d : _totalElapsedTime / _totalTimeMessages / 1000d;
var avgLogicalReads = _totalReadMessages == 0 ? 0d : _totalLogicalReads / _totalReadMessages;

return new RunResult
{
TestId = _testGuid,
StartTime = _testStartTime.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture),
ElapsedTime = elapsed,
Threads = _numThreads,
Iterations = _settings.NumIterations,
CompletedIterations = _totalIterations,
Delay = _settings.DelayBetweenQueries,
AvgCpuSeconds = avgCpuSeconds,
AvgActualSeconds = avgActualSeconds,
AvgClientSeconds = avgClientSeconds,
AvgLogicalReads = avgLogicalReads,
ExceptionCount = _totalExceptions
};
}

private void ExportBenchMarkToCsvFile(string fileName)
private bool ExportBenchMarkToCsvFile(string fileName)
{
try
{
Expand All @@ -289,11 +367,12 @@ private void ExportBenchMarkToCsvFile(string fileName)
WriteBenchmarkCsvHeader(textWriter);
}
WriteBenchmarkCsvText(textWriter);
return true;
}
catch (Exception)
{
Console.Error.WriteLine("Error While Saving BenchMark",
$"There was an error saving the benchmark to '{fileName}', make sure you have write privileges to that path");
Console.Error.WriteLine($"There was an error saving the benchmark to '{fileName}', make sure you have write privileges to that path");
return false;
}
}

Expand All @@ -306,10 +385,10 @@ private void WriteBenchmarkCsvText(TextWriter tw)
{
tw.WriteLine("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10}",
_testGuid,
_testStartTime,
_testStartTime.ToString("O", CultureInfo.InvariantCulture),
theTime,
_totalIterations,
_runParameters.NumberOfThreads,
_runParameters.NumberOfThreads ?? _numThreads,
_settings.DelayBetweenQueries,
_totalIterations,
cpuTime,
Expand Down
30 changes: 19 additions & 11 deletions src/SqlQueryStressCLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,43 @@ namespace SqlQueryStressCLI
{
class Program
{
static void Main(string[] args)
static int Main(string[] args)
{
var options = new CommandLineOptions();
var parserResult = Parser.Default.ParseArguments<CommandLineOptions>(args);

parserResult
.WithParsed(options => Run(options))
.WithNotParsed(errors => DisplayHelp(parserResult, errors));
return parserResult.MapResult(
Run,
errors =>
{
DisplayHelp(parserResult, errors);
return errors.IsHelp() || errors.IsVersion() ? 0 : 1;
});
}

private static void Run(CommandLineOptions options)
private static int Run(CommandLineOptions options)
{
if (options.ExtractSample)
{
ExtractSample();
return 0;
}

if (File.Exists(options.SettingsFile))
{
var settings = OpenConfigFile(options.SettingsFile);

var runner = new LoadRunner(settings, options);
if (settings == null)
{
return 1;
}

runner.Run();
}
else
{
Console.Error.WriteLine($"Settings file could not be found, or not specified: {options.SettingsFile}");
var runner = new LoadRunner(settings, options);
return runner.Run() ? 0 : 1;
}

Console.Error.WriteLine($"Settings file could not be found, or not specified: {options.SettingsFile}");
return 1;
}

private static void ExtractSample()
Expand Down
9 changes: 9 additions & 0 deletions src/SqlQueryStressCLI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ sqlstresscmd -x
sqlstresscmd -s sample.json -t 1
```

To save a machine-readable result file, use `-r`/`--results` and optionally `-f`/`--format`:

```bash
sqlstresscmd -s sample.json -t 1 -r results.json -f json
sqlstresscmd -s sample.json -t 1 -r results.csv
```

The CSV output keeps the existing append-friendly format, while JSON writes a single structured run summary with invariant culture formatting and ISO-8601 timestamps.

![Sample screenshot](https://raw.githubusercontent.com/ErikEJ/SqlQueryStress/refs/heads/master/src/SqlQueryStressCLI/sample.png)

To get help, run
Expand Down
1 change: 1 addition & 0 deletions src/SqlQueryStressCLI/sqlstresscmd.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Compile Include="..\SQLQueryStress\LoadEngine.cs" Link="LoadEngine.cs" />
<Compile Include="..\SQLQueryStress\QueryStressSettings.cs" Link="QueryStressSettings.cs" />
<Compile Include="..\SQLQueryStress\ConnectionSettingsHelper.cs" Link="ConnectionSettingsHelper.cs" />
<Compile Include="..\SQLQueryStress\RunResult.cs" Link="RunResult.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading