Outputs
CSV Output Format
Introduction
Sailfish can generate comprehensive CSV files containing both individual test results and method comparison data using the [WriteToCsv] attribute. These files use a structured, multi-section format that's Excel-friendly and perfect for data analysis.
Basic Usage
Apply the [WriteToCsv] attribute to any test class:
[WriteToCsv][Sailfish(SampleSize = 100)]public class SortBenchmarks{ [SailfishMethod(IsBaseline = true)] public void QuickSort() { /* implementation */ }
[SailfishMethod] public void BubbleSort() { /* implementation */ }
[SailfishMethod] public void MergeSort() { /* implementation */ }}The [Sailfish] class above forms an implicit class-wide comparison group — QuickSort is the baseline; the other two are contenders. Pass DisableComparison = true to [Sailfish] to opt out and just emit per-method rows.
CSV Structure
The generated CSV file uses a two-section format. Each section starts with a #-prefixed comment header.
Section 1: Individual Test Results
# Individual Test ResultsTestClass,TestMethod,MeanTime,MedianTime,StdDev,SampleSize,ComparisonGroup,StatusSortBenchmarks,BubbleSort,45.200,44.100,3.100,100,SortBenchmarks,SuccessSortBenchmarks,MergeSort,3.400,3.300,0.300,100,SortBenchmarks,SuccessSortBenchmarks,QuickSort,2.100,2.000,0.300,100,SortBenchmarks,SuccessFields:
- TestClass: Name of the test class
- TestMethod: Name of the test method
- MeanTime: Average execution time in milliseconds
- MedianTime: Median execution time in milliseconds
- StdDev: Standard deviation of execution times
- SampleSize: Number of iterations executed
- ComparisonGroup: The comparison-group label for this method's row.
- For methods in the implicit class-wide group (the default when
[Sailfish]is set withoutDisableComparison): the class name. - For methods with explicit
[SailfishMethod(ComparisonGroup = "...")]: that name. - Empty for methods not in any comparison group (their class has
DisableComparison = true). - Comparison groups are scoped per class — the same name in two different classes is reported as two independent groups, distinguished by the
TestClasscolumn.
- For methods in the implicit class-wide group (the default when
- Status: Test execution status (
Success/Failed)
Where are CI95_MOE / CI99_MOE?
The session CSV intentionally keeps the per-test row narrow. The CI95 / CI99 margin-of-error columns are emitted in the per-class tracking CSV (next to the tracking JSON used by SailDiff) and surfaced in the Reproducibility Manifest.
Section 2: Method Comparisons
The shape of this section depends on whether the group has a baseline:
- Baseline mode (
IsBaseline = trueon one member): N−1 rows. Each row compares the baseline (Method1) to one contender (Method2). - N×N mode (no baseline): N×(N−1)/2 rows. Every unique pair appears once.
# Method ComparisonsComparisonGroup,Method1,Method2,Mean1,Mean2,Ratio,CI95_Lower,CI95_Upper,q_value,Label,ChangeDescriptionSortBenchmarks,QuickSort,BubbleSort,2.100,45.200,21.524,18.301,24.917,1.2e-12,Slower,RegressedSortBenchmarks,QuickSort,MergeSort,2.100,3.400,1.619,1.412,1.856,3.4e-09,Slower,RegressedFields:
- ComparisonGroup: The comparison-group label — class name for the implicit class-wide group, or the explicit name set via
SailfishMethod(ComparisonGroup = "..."). - Method1 / Method2: The two methods being compared. In baseline mode, Method1 is always the baseline.
- Mean1 / Mean2: Mean execution times (ms) for each method
- Ratio:
Mean2 / Mean1(unitless). Values > 1 indicate Method 2 is slower than Method 1; values < 1 indicate Method 2 is faster. - CI95_Lower / CI95_Upper: 95% confidence interval endpoints for the ratio (computed on the log scale)
- q_value: Benjamini–Hochberg adjusted p-value across the group (multiple comparisons correction)
- Label: One of Improved, Similar, or Slower at α = 0.05
- ChangeDescription: Legacy summary for backward compatibility (Improved / Regressed / No Change)
Session-Based Consolidation
CSV files use session-based consolidation, meaning:
- Single file per session: All test classes with
[WriteToCsv]contribute to one file - Cross-class comparisons: Method comparisons work across different test classes
- Unique naming: Files use session IDs and timestamps to prevent conflicts
- Complete data: All test results from the entire session are included
Example filename: TestSession_abc12345_Results_20250803_103000.csv
Excel Integration
The CSV format is designed for easy Excel analysis:
1. Import Process
- Open Excel
- Go to Data → Get Data → From Text/CSV
- Select your Sailfish CSV file
- Excel will automatically detect the structure
2. Working with Sections
- Comment lines (starting with
#) provide clear section headers - Consistent column structure within each section
- No mixed data types in columns for reliable sorting/filtering
3. Analysis Examples
Performance Analysis:
=AVERAGE(C:C) // Average mean time across all tests=MAX(C:C) // Slowest test=MIN(C:C) // Fastest testComparison Analysis:
// Filter Method Comparisons section// Sort by Ratio to find biggest differences// Create charts showing performance relationshipsAdvanced Features
Multiple Comparison Groups
A class can declare explicit ComparisonGroup names to split its methods into more than one comparison. Each group generates its own set of rows:
# Method ComparisonsComparisonGroup,Method1,Method2,Mean1,Mean2,Ratio,CI95_Lower,CI95_Upper,q_value,Label,ChangeDescriptionStringOperations,StringBuilder,StringConcat,8.100,15.200,1.877,1.689,2.085,1.0e-12,Slower,RegressedStringOperations,StringBuilder,StringInterpolation,8.100,12.300,1.519,1.371,1.682,2.3e-08,Slower,RegressedStringOperations,StringConcat,StringInterpolation,15.200,12.300,0.809,0.731,0.895,3.4e-04,Improved,ImprovedCollections,ArrayIteration,ListIteration,3.200,5.400,1.688,1.523,1.870,4.1e-10,Slower,RegressedThe rows above are an N×N example — none of the methods is marked IsBaseline. Add IsBaseline = true to one method per group and the section shrinks to N−1 rows, with Method1 always being that baseline.
Pair counts per group
| Methods in group | No-baseline (N×N) rows | Baseline (N−1) rows |
|---|---|---|
| 2 | 1 | 1 |
| 3 | 3 | 2 |
| 4 | 6 | 3 |
| N | N × (N−1) / 2 | N − 1 |
Baseline mode is set by adding IsBaseline = true to exactly one [SailfishMethod] in the group. The CSV row count for that group shrinks accordingly and the FDR adjustment runs over the smaller set, sharpening individual q-values.
Classes that opt out of comparison
Use [Sailfish(DisableComparison = true)] when a class isn't really comparing alternatives — methods then appear in the individual results section with an empty ComparisonGroup column and no comparison rows are emitted:
# Individual Test ResultsTestClass,TestMethod,MeanTime,MedianTime,StdDev,SampleSize,ComparisonGroup,StatusSmokeChecks,OperationA,10.500,9.800,1.200,100,,SuccessSmokeChecks,OperationB,1.100,1.000,0.100,100,,SuccessBest Practices
1. Organize Your Data
Use meaningful test class and method names since they appear in the CSV:
[WriteToCsv][Sailfish]public class DatabaseQueryPerformance // Class name doubles as the implicit comparison group label{ [SailfishMethod(IsBaseline = true)] public void SimpleSelect() { } // Descriptive method name
[SailfishMethod] public void ComplexJoin() { } // Descriptive method name}2. Reach for explicit groups only when you need multiple in one class
The implicit class-wide group is usually all the context the output needs — methods in DatabaseQueryPerformance already group under that class. Only set ComparisonGroup = "..." when one class genuinely has multiple distinct comparisons:
[SailfishMethod(ComparisonGroup = "DatabaseQueries")] // Good — explicit name needed for multi-group class[SailfishMethod(ComparisonGroup = "SerializationMethods")] // Good[SailfishMethod(ComparisonGroup = "Group1")] // Poor — meaningless name3. Configure Output Directory
Set a consistent output directory for organized results:
var runner = SailfishRunner.CreateBuilder() .WithRunSettings(settings => settings .WithLocalOutputDirectory("./performance-results")) .Build();4. Combine with Markdown
Use both output formats for comprehensive reporting:
[WriteToMarkdown] // Human-readable reports[WriteToCsv] // Data analysis[Sailfish]public class ComprehensiveTest { }Troubleshooting
Empty CSV Files
If CSV files are empty or missing:
- Check attribute placement: Ensure
[WriteToCsv]is on the test class, not methods - Verify test execution: CSV is only generated after successful test completion
- Check output directory: Verify the configured output directory exists and is writable
Missing Comparisons
If method comparisons are missing from the CSV:
- Class is
[Sailfish]: comparison rows require the class to be a Sailfish test class. - Not opted out: check whether the class has
DisableComparison = trueon[Sailfish]. - Method count: each comparison group needs ≥ 2 methods (SF1302 warns at build time).
- Explicit group names match: when using explicit
ComparisonGroup, values are case-sensitive. - Single baseline per group: at most one method per group can set
IsBaseline = true(SF1301 enforces this).
Excel Import Issues
If Excel doesn't import correctly:
- Check file encoding: Ensure CSV is saved as UTF-8
- Verify delimiters: Use comma delimiters consistently
- Handle comments: Excel may need manual handling of
#comment lines
Integration Examples
CI/CD Pipeline
- name: Run Performance Tests run: dotnet test --logger "console;verbosity=detailed"
- name: Upload CSV Results uses: actions/upload-artifact@v3 with: name: performance-results path: "**/TestSession_*.csv"Automated Analysis
// Read and analyze CSV results programmaticallyvar csvData = File.ReadAllText("TestSession_abc12345_Results_20250803_103000.csv");var results = ParseSailfishCsv(csvData);
// Generate reports, alerts, or dashboardsif (results.HasRegressions){ SendAlert($"Performance regression detected: {results.WorstRegression}");}