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
18 changes: 6 additions & 12 deletions NBomber.Data.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,34 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36930.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NBomber.Data", "src\NBomber.Data\NBomber.Data.fsproj", "{5EDFA30B-C515-4810-A0BB-6A9AB57518B5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{08F0A3DA-CDB0-4629-8A02-116A89DDBC44}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharp", "examples\CSharp\CSharp.csproj", "{1D5140A9-335C-4615-9EED-2C970D0C6A60}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBomber.LargeData", "src\NBomber.LargeData\NBomber.LargeData.csproj", "{51A87D87-CB65-4A13-B452-5352129AF26C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBomber.Data.Tests", "tests\NBomber.Data.Tests\NBomber.Data.Tests.csproj", "{074C82C1-70BC-45FB-8B87-3D44CBD43F08}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NBomber.Data", "src\NBomber.Data\NBomber.Data.csproj", "{C83B7AAF-409C-DC35-7028-EE43939D2AFC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5EDFA30B-C515-4810-A0BB-6A9AB57518B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EDFA30B-C515-4810-A0BB-6A9AB57518B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EDFA30B-C515-4810-A0BB-6A9AB57518B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5EDFA30B-C515-4810-A0BB-6A9AB57518B5}.Release|Any CPU.Build.0 = Release|Any CPU
{1D5140A9-335C-4615-9EED-2C970D0C6A60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D5140A9-335C-4615-9EED-2C970D0C6A60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D5140A9-335C-4615-9EED-2C970D0C6A60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D5140A9-335C-4615-9EED-2C970D0C6A60}.Release|Any CPU.Build.0 = Release|Any CPU
{51A87D87-CB65-4A13-B452-5352129AF26C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51A87D87-CB65-4A13-B452-5352129AF26C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51A87D87-CB65-4A13-B452-5352129AF26C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51A87D87-CB65-4A13-B452-5352129AF26C}.Release|Any CPU.Build.0 = Release|Any CPU
{074C82C1-70BC-45FB-8B87-3D44CBD43F08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{074C82C1-70BC-45FB-8B87-3D44CBD43F08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{074C82C1-70BC-45FB-8B87-3D44CBD43F08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{074C82C1-70BC-45FB-8B87-3D44CBD43F08}.Release|Any CPU.Build.0 = Release|Any CPU
{C83B7AAF-409C-DC35-7028-EE43939D2AFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C83B7AAF-409C-DC35-7028-EE43939D2AFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C83B7AAF-409C-DC35-7028-EE43939D2AFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C83B7AAF-409C-DC35-7028-EE43939D2AFC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
9 changes: 4 additions & 5 deletions examples/CSharp/CSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@
<WarningsAsErrors>(WarningsAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\NBomber.Data\NBomber.Data.fsproj" />
<ProjectReference Include="..\..\src\NBomber.LargeData\NBomber.LargeData.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="users-feed-data.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand All @@ -39,4 +34,8 @@
<PackageReference Include="NBomber" Version="6.1.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\NBomber.Data\NBomber.Data.csproj" />
</ItemGroup>

</Project>
109 changes: 109 additions & 0 deletions src/NBomber.Data/CircularLargeDataFeed.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using NBomber.Contracts;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace NBomber.LargeData
{
internal class CircularLargeDataFeed<T> : IAsyncDataFeed<T>, IAsyncDisposable
{
private const int BatchCount = 4;
private readonly int _batchSize;
private readonly SqliteDbRepository<T> _db = new SqliteDbRepository<T>();
private readonly object _switchLock = new object();
private readonly List<T>[] _batches;
private volatile int _activeBatchIndex = 0;
private int _currentIndexInBatch = -1;
private long _nextDbIdToLoad = 1;
private Task? _nextBatchLoadTask;
private Serilog.ILogger? _logger;
private bool _isSmallDataset;

public CircularLargeDataFeed(int elementsInMemoryCount)
{
elementsInMemoryCount = elementsInMemoryCount > 100 ? elementsInMemoryCount : 100;
_batchSize = elementsInMemoryCount / BatchCount;
_batches = new List<T>[BatchCount];
for (int i = 0; i < BatchCount; i++)
_batches[i] = new List<T>(_batchSize);
}

public void LoadData(Serilog.ILogger logger, IEnumerable<T> data)
{
_logger = logger;
_db.LoadData(data);

if (_db.DataCount == 0)
throw new InvalidOperationException("Data source is empty. At least one item is required.");

_isSmallDataset = _db.DataCount < _batchSize;

for (int i = 0; i < BatchCount; i++)
_nextDbIdToLoad = _db.LoadBatch(_batches[i], _nextDbIdToLoad, _batchSize);
}

public async ValueTask<T> GetNextItem(ScenarioInfo scenarioInfo)
{
while (true)
{
var index = Interlocked.Increment(ref _currentIndexInBatch);

if (index < _batchSize)
{
var batch = _batches[_activeBatchIndex];
var item = batch[index];
return item;
}

// Batch exhausted - need to switch
Task? currentBatchLoadTask = null;

lock (_switchLock)
{
// Double-check after acquiring lock
if (_currentIndexInBatch >= _batchSize)
{
currentBatchLoadTask = _nextBatchLoadTask;

// Get the batch that was just exhausted to reload in background
var exhaustedBatchIndex = _activeBatchIndex;

// Switch to next batch
_activeBatchIndex++;
if (_activeBatchIndex >= BatchCount)
_activeBatchIndex = 0;

// For small datasets, both batches are pre-filled with repeated copies
// of all data, so we don't need to reload - just switch between them
if (!_isSmallDataset)
{
// Start loading the exhausted batch in background
_nextBatchLoadTask = Task.Run(() => _nextDbIdToLoad = _db.LoadBatch(_batches[exhaustedBatchIndex], _nextDbIdToLoad, _batchSize));
}

// Reset index
_currentIndexInBatch = -1;
}
}

// Wait OUTSIDE the lock to allow other threads to proceed
if (currentBatchLoadTask != null)
{
if (currentBatchLoadTask.Status != TaskStatus.RanToCompletion)
_logger?.Warning("You should use bigger elementsInMemoryCount, because in memory items were exhausted too fast");

await currentBatchLoadTask;
}
}
}

public ValueTask DisposeAsync()
{
if (_db != null)
_db.DisposeAsync();

return default;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using NBomber.Contracts;
using NBomber.Contracts;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace NBomber.LargeData;

Expand All @@ -22,10 +25,10 @@ public ValueTask<T> GetNextItem(ScenarioInfo scenarioInfo)
// Check if ID is within pre-loaded batch range (1 to _cachedBatchEndId)
if (id <= _cachedBatchEndId)
{
return ValueTask.FromResult(_cachedBatch[(int)(id - 1)]);
return new ValueTask<T>(_cachedBatch[(int)(id - 1)]);
}

return ValueTask.FromResult(_db.GetById(id));
return new ValueTask<T>(_db.GetById(id));
}

public void LoadData(Serilog.ILogger logger, IEnumerable<T> data)
Expand All @@ -42,9 +45,11 @@ public void LoadData(Serilog.ILogger logger, IEnumerable<T> data)
_cachedBatchEndId = batchSize;
}

public async ValueTask DisposeAsync()
public ValueTask DisposeAsync()
{
if (_db != null)
_db.DisposeAsync();

return default;
}
}
69 changes: 69 additions & 0 deletions src/NBomber.Data/Data.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using CsvHelper;

namespace NBomber.Data;

/// <summary>
/// Provides utility functions for generating random data and loading structured data from JSON or CSV sources.
/// Supports both local file paths and remote HTTP URLs.
/// </summary>
public static class Data
{
[ThreadStatic]
private static Random? _random;
private static Random RandomInstance => _random ??= new Random();

/// <summary>
/// Generates a random byte array of the specified size.
/// </summary>
/// <param name="sizeInBytes">The number of bytes to generate.</param>
/// <returns>An array of random bytes.</returns>
public static byte[] GenerateRandomBytes(int sizeInBytes)
{
var buffer = new byte[sizeInBytes];
RandomInstance.NextBytes(buffer);
return buffer;
}

/// <summary>
/// Loads and deserializes a JSON document from a local file or an HTTP URL into a value of type <typeparamref name="T"/>.
/// </summary>
/// <param name="path">The full path to the local JSON file or a valid HTTP/HTTPS URL.</param>
/// <typeparam name="T">The target type into which the JSON will be deserialized.</typeparam>
/// <returns>An instance of <typeparamref name="T"/> populated with the deserialized JSON data.</returns>
public static T? LoadJson<T>(string path)
{
using var stream = GetStream(path);
return JsonSerializer.Deserialize<T>(stream);
}

/// <summary>
/// Loads and parses a CSV file from a local file or an HTTP URL into an array of items of type <typeparamref name="T"/>.
/// </summary>
/// <param name="path">The full path to the local CSV file or a valid HTTP/HTTPS URL.</param>
/// <typeparam name="T">The type each row in the CSV is mapped to.</typeparam>
/// <returns>An array of items of type <typeparamref name="T"/> parsed from the CSV data.</returns>
public static T[] LoadCsv<T>(string path)
{
using var stream = GetStream(path);
using var reader = new StreamReader(stream);
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
return csv.GetRecords<T>().ToArray();
}

private static Stream GetStream(string path)
{
if (Uri.IsWellFormedUriString(path, UriKind.Absolute))
{
using var client = new HttpClient();
return client.GetStreamAsync(path).GetAwaiter().GetResult();
}

return File.OpenRead(path);
}
}
64 changes: 0 additions & 64 deletions src/NBomber.Data/Data.fs

This file was deleted.

Loading
Loading