Skip to content

Main #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
112 changes: 112 additions & 0 deletions RESULT_2025.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Async Runtimes Benchmarks 2025

## Benchmark Evolution and Methodology

### Purpose

This benchmark aims to measure the memory footprint and performance characteristics of concurrent tasks across different programming language runtimes in 2025.

### Benchmark Specification

- Spawn N concurrent tasks (controlled by command-line argument)
- Default task count: 100,000
- Each task waits for 10 seconds
- Program exits when all tasks complete

## Improvements in 2025 Benchmark

### Standardization Efforts

1. **Consistent Task Implementation**
- Explicit task creation mechanism - 2024 previous implementations vary slightly in how they create and manage tasks. We should standardize.
- Standardized 10-second wait
- Detailed task result tracking
- Comprehensive error handling
- Detailed performance tracking

2. **Language-Specific Enhancements**
- Python (asyncio):
- Explicit task creation with `asyncio.create_task()`
- Comprehensive error handling
- Detailed task result tracking

- Node.js:
- Promise-based task creation
- Error handling with `Promise.allSettled()`
- Detailed task duration tracking

- Go:
- Context-based timeout management
- Structured goroutine approach
- Error tracking and reporting

- Rust (Tokio):
- Detailed task tracking
- Cancellation support
- Comprehensive error handling

- Kotlin (Coroutines):
- TaskResult data class for comprehensive result tracking
- Kotlin coroutines for concurrent task management
- Detailed timing with Instant and Duration
- coroutineScope for structured concurrency
- awaitAll() for comprehensive task completion
- Flexible task count with default of 100,000
- Detailed result printing with task ID and duration

- C# (.NET Tasks):
- Introduced a record TaskResult for structured result tracking
- Used Task.Run() for concurrent task creation
- Implemented detailed timing with DateTime and TimeSpan
- Used ConcurrentBag for thread-safe result collection
- Comprehensive error handling
- Flexible task count with default of 100,000
- Optional result printing with task details

- Elixir (Processes):
- Created a custom struct for task result tracking
- Used Elixir's lightweight processes for concurrency
- Implemented detailed timing with system time
- Used spawn_link for process creation
- Message passing for result collection
- Flexible task count with default of 100,000
- Optional result printing with task ID and duration

### Benchmarking Recommendations

## Overarching Recommendations:

- Standardize memory tracking across languages
- Add detailed logging and tracing
- Implement consistent performance metrics
- Create a unified result collection and reporting mechanism
- Add configuration for different benchmark scenarios

1. **Consistent Environment**
- Use identical hardware
- Standardized OS and runtime versions
- Minimal background processes

2. **Metrics Collection**
- Memory usage
- CPU utilization
- Task creation overhead
- Total execution time
- Garbage collection impact

### Methodology Notes

- Multiple iterations for statistical significance
- Warm-up runs to stabilize runtime environments
- Detailed logging of task performance

### Future Improvements

- Implement similar benchmarking approach
- Add more language runtimes
- Implement more sophisticated memory profiling
- Create visualization tools for result comparison

## Conclusion

This benchmark provides insights into the concurrent task handling capabilities of different language runtimes, highlighting the evolving landscape of asynchronous programming in 2025.
162 changes: 155 additions & 7 deletions dotnet/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,158 @@
int numTasks = int.Parse(args[0]);
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

List<Task> tasks = new List<Task>(numTasks);

for (int i = 0; i < numTasks; i++)
/// <summary>
/// Advanced .NET 9 Async Tasks Benchmark
///
/// Key Features:
/// - Leveraging .NET 9 performance improvements
/// - Native AOT compilation support
/// - Advanced memory profiling
/// - Detailed logging
/// - Configurable concurrency
/// </summary>
public class AsyncTasksBenchmark
{
tasks.Add(Task.Delay(TimeSpan.FromSeconds(10)));
}
/// <summary>
/// Detailed task performance metrics with .NET 9 enhancements
/// </summary>
public record TaskMetrics(
int TaskId,
DateTime StartTime,
DateTime EndTime,
long StartMemory,
long EndMemory)
{
/// <summary>
/// Calculate task duration using .NET 9 time abstractions
/// </summary>
public TimeSpan Duration => EndTime - StartTime;

/// <summary>
/// Calculate memory change with improved precision
/// </summary>
public long MemoryChange => EndMemory - StartMemory;
}

/// <summary>
/// Advanced memory profiler optimized for .NET 9
/// </summary>
private static class MemoryProfiler
{
/// <summary>
/// Get current process memory usage with .NET 9 improvements
/// </summary>
public static long GetMemoryUsage()
{
// Leverage .NET 9 cross-platform memory tracking
return GC.GetTotalMemory(false);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should rely on external tools to measure the memory. GC.GetTotalMemory only accounts for managed memory which doesn't include the runtime overhead.

}
}

/// <summary>
/// Perform an asynchronous task with comprehensive tracking
/// </summary>
private static async Task<TaskMetrics> PerformTaskAsync(
int taskId,
ILogger logger)
{
// Leverage .NET 9's improved task tracking
var startTime = DateTime.UtcNow;
var startMemory = MemoryProfiler.GetMemoryUsage();

try
{
// Simulate 10-second task with improved delay mechanism
await Task.Delay(TimeSpan.FromSeconds(10));

var endTime = DateTime.UtcNow;
var endMemory = MemoryProfiler.GetMemoryUsage();

// Advanced logging with structured logging
logger.LogInformation(
"Task {TaskId} completed. Duration: {Duration}ms, Memory Change: {MemoryChange} bytes",
taskId,
(endTime - startTime).TotalMilliseconds,
endMemory - startMemory
);

return new TaskMetrics(
TaskId: taskId,
StartTime: startTime,
EndTime: endTime,
StartMemory: startMemory,
EndMemory: endMemory
);
}
catch (Exception ex)
{
// Enhanced error handling with .NET 9 logging
logger.LogError(
ex,
"Task {TaskId} failed with error: {ErrorMessage}",
taskId,
ex.Message
);

throw;
}
}

/// <summary>
/// Run benchmark with configurable task count
/// </summary>
private static async Task RunBenchmarkAsync(
int numTasks,
ILogger logger)
{
// Leverage .NET 9's improved concurrent collections
var results = new ConcurrentBag<TaskMetrics>();

// Parallel task execution with improved task management
var tasks = Enumerable.Range(0, numTasks)
.Select(taskId => Task.Run(async () =>
{
var result = await PerformTaskAsync(taskId, logger);
results.Add(result);
return result;
}))
.ToList();

// Efficient task completion tracking
await Task.WhenAll(tasks);
Comment on lines +113 to +126
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are capturing locals in the closure and creating unnecessary delegates.
Also, unlike Rust, by default .NET schedules async Tasks and yields immediately, so you don't need to wrap it in a Task.Run at all (which would result in creating two Tasks for every single async Task). And the usage to ConcurrentBag is unnecessary.
The code should be simple to avoid introducing extra overhead which is unrelated to the concurrency structure itself.

Suggested change
var results = new ConcurrentBag<TaskMetrics>();
// Parallel task execution with improved task management
var tasks = Enumerable.Range(0, numTasks)
.Select(taskId => Task.Run(async () =>
{
var result = await PerformTaskAsync(taskId, logger);
results.Add(result);
return result;
}))
.ToList();
// Efficient task completion tracking
await Task.WhenAll(tasks);
var tasks = new List<Task<TaskMetrics>>(numTasks);
for (int i = 0; i < numTasks; i++)
{
tasks.Add(PerformTaskAsync(i, logger));
}
await Task.WhenAll(results); // this will gives you TaskMetrics[] which contains all the results


// Comprehensive benchmark summary
logger.LogInformation(
"Benchmark completed. Total Tasks: {TotalTasks}, Successful Tasks: {SuccessfulTasks}",
numTasks,
results.Count
);
}

/// <summary>
/// Main entry point with Native AOT support
/// </summary>
public static async Task Main(string[] args)
{
// Improved logging configuration
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddConsole()
.SetMinimumLevel(LogLevel.Information);
});
var logger = loggerFactory.CreateLogger<AsyncTasksBenchmark>();

// Parse task count with improved parsing
int numTasks = args.Length > 0
? int.Parse(args[0])
: 100_000;

await Task.WhenAll(tasks);
// Benchmark execution
await RunBenchmarkAsync(numTasks, logger);
}
}
29 changes: 29 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# .NET Async Tasks Benchmark

## Performance Characteristics

### Why .NET?

- High-performance async/await model
- Cross-platform runtime
- Advanced memory management
- Support for multiple concurrency models

### Benchmark Methodology

- Spawn 100,000 concurrent tasks
- Each task sleeps for 10 seconds
- Track memory and execution metrics

## Performance Recommendations

1. Use .NET 8.0+ runtime
2. Enable tiered compilation
3. Use Release configuration
4. Consider native AOT compilation

### Execution

```bash
dotnet run -c Release [num_tasks]
```
15 changes: 12 additions & 3 deletions dotnet/dotnet.csproj
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<InvariantGlobalization>true</InvariantGlobalization>

<!-- .NET 9 specific optimizations -->
<PublishTrimmed>true</PublishTrimmed>
<PublishAot>true</PublishAot>
<TrimmerDefaultAction>link</TrimmerDefaultAction>
Comment on lines -8 to +11
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need the globalization here, which aligns with the behavior of other languages. Otherwise, it would lead the runtime to load ICU globalization data into the memory.
Also, when PublishAot is true, the trimmer behavior is implied so you don't need to set PublishTrimmed or TrimmerDefaultAction at all.

</PropertyGroup>

</Project>
<ItemGroup>
<!-- .NET 9 preview packages -->
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="9.0.0-preview.1.*" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0-preview.1.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0-preview.1.*" />
Comment on lines +16 to +18
Copy link
Owner

@hez2010 hez2010 Dec 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using preview versions while the 9.0 GA versions are present?
And I don't see why a logger is needed when you can just use Console.WriteLine.

</ItemGroup>
</Project>
56 changes: 56 additions & 0 deletions elixir/main.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule AsyncBenchmark do
defstruct [:task_id, :start_time, :end_time, :duration]

def perform_task(task_id) do
start_time = :os.system_time(:millisecond)

# Simulate 10-second task
:timer.sleep(10000)

end_time = :os.system_time(:millisecond)
duration = end_time - start_time

%__MODULE__{
task_id: task_id,
start_time: start_time,
end_time: end_time,
duration: duration
}
end

def spawn_tasks(num_tasks) do
parent = self()

tasks = Enum.map(0..(num_tasks - 1), fn task_id ->
spawn_link(fn ->
result = perform_task(task_id)
send(parent, {:task_result, result})
end)
end)

# Collect results
results = Enum.map(0..(num_tasks - 1), fn _ ->
receive do
{:task_result, result} -> result
end
end)

# Optional: Print results
Enum.each(results, fn result ->
IO.puts "Task #{result.task_id} completed in #{result.duration / 1000} seconds"
end)
end

def main(args \\ []) do
# Parse number of tasks, default to 100,000
num_tasks = case args do
[num_str] -> String.to_integer(num_str)
_ -> 100_000
end

spawn_tasks(num_tasks)
end
end

# Run the benchmark
AsyncBenchmark.main(System.argv())
Loading