Hace unos días se han anunciado las novedades de la .Net 7, en este caso, la versión preview 4. Las mejoras más destacables están relacionadas con observabilidad de la mano de OpenTelemetry con la posibilidad de hacer seguimiento en microsegundos y nanosegundos con fechas y horas, extensiones para almacenar métricas en caché, aumento del rendimiento, mejoras en las apis para trabajar con archivos y mucho más. En este post veremos las más interesantes.
Recuerda que puedes probar esta versión desde los links oficiales. haz clic aquí. ¡Comencemos!
Nullable annotations para Microsoft.Extensions
Se ha completado todo el soporte para tipos nullables para la librería Microsoft.Extension. Más allá de la novedad, quiero destacar el aporte de @maxkoshevoi‘s que desde hace mucho tiempo viene trabajando en el tema y no pertenece al equipo de Microsoft si no que es alguien que participa en la comunidad.
Observabilidad
Activity.Current
Usamos AsyncLocal<T> para realizar un seguimiento del «contexto de span» de los subprocesos administrados. Los cambios en el contexto del intervalo se realizan mediante el AsyncLocal<T> constructor que toma el parámetro valueChangedHandler. Pero, con Activity como estándar para representar estos spans, como en OpenTelemetry, no es posible establecer el controlador de valor porque se realiza un seguimiento del contexto via Activity.Current. Podemos usar el nuevo evento de cambio en su lugar para recibir las notificaciones deseadas.
public partial class Activity : IDisposable
{
public static event EventHandler<ActivityChangedEventArgs>? CurrentChanged;
}
Activity.CurrentChanged += CurrentChanged;
void CurrentChanged(object? sender, ActivityChangedEventArgs e)
{
Console.WriteLine($"Activity.Current value changed from Activity: {e.Previous.OperationName} to Activity: {e.Current.OperationName}");
}
Enumeraciones en Activity
Los métodos ahora expuestos, se pueden usar en los escenarios críticos de rendimiento para enumerar las propiedades Etiquetas de actividad, Vínculos y Eventos sin asignaciones adicionales y con acceso rápido a los elementos.
namespace System.Diagnostics
{
partial class Activity
{
public Enumerator<KeyValuePair<string,object>> EnumerateTagObjects();
public Enumerator<ActivityLink> EnumerateLinks();
public Enumerator<ActivityEvent> EnumerateEvents();
public struct Enumerator<T>
{
public readonly Enumerator<T> GetEnumerator();
public readonly ref T Current;
public bool MoveNext();
}
}
}
Activity a = new Activity("Root");
a.SetTag("key1", "value1");
a.SetTag("key2", "value2");
foreach (ref readonly KeyValuePair<string, object?> tag in a.EnumerateTagObjects())
{
Console.WriteLine($"{tag.Key}, {tag.Value}");
}
Microseconds y Nanoseconds para TimeStamp, DateTime, DateTimeOffset, y TimeOnly
Antes de esta versión, el rango más bajo de tiempo disponible en las estructuras de fecha y hora era el «tick» disponible en la propiedad Ticks que era de 100ns. Para hacer obtener valores en microsegundos o nanosegundos debíamos hacer cálculos sobre ese valor tick. En esta versión se han implementado para poder trabajar con esto.
namespace System {
public struct DateTime {
public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond);
public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.DateTimeKind kind);
public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.Globalization.Calendar calendar);
public int Microsecond { get; }
public int Nanosecond { get; }
public DateTime AddMicroseconds(double value);
}
public struct DateTimeOffset {
public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.TimeSpan offset);
public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.TimeSpan offset, System.Globalization.Calendar calendar);
public int Microsecond { get; }
public int Nanosecond { get; }
public DateTimeOffset AddMicroseconds(double microseconds);
}
public struct TimeSpan {
public const long TicksPerMicrosecond = 10L;
public const long NanosecondsPerTick = 100L;
public TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds, int microseconds);
public int Microseconds { get; }
public int Nanoseconds { get; }
public double TotalMicroseconds { get; }
public double TotalNanoseconds { get; }
public static TimeSpan FromMicroseconds(double microseconds);
}
public struct TimeOnly {
public TimeOnly(int hour, int minute, int second, int millisecond, int microsecond);
public int Microsecond { get; }
public int Nanosecond { get; }
}
}
More improvements and new APIs for System.Text.RegularExpressions
Se han agregado y se agregaran api para darle soporte a Span a la libreria de Regex. Las principales que se han agregado ahora son:
- Regex.IsMatch(ReadOnlySpan<char> input): Indicate si la expresión regular encuentra una coincidencia en el intervalo de entrada.
- Regex.Count(ReadOnlySpan<char> input): Busca en una cadena de entrada todas las apariciones de una expresión regular y devuelve el número de coincidencias.
- Regex.EnumerateMatches(ReadOnlySpan<char> input): Busca en un intervalo de entrada todas las apariciones de una expresión regular y devuelve un ValueMatchEnumerator para iterar perezosamente sobre las coincidencias.
Se han optimizado también el rendimiento general como: el manejo de conjuntos Regex más comunes, en la lógica que encuentra posibles posiciones donde podría existir una comparación, evitar asignaciones siempre que sea posible, lo que hace que el motor vaya más rápido y mejoras en la lógica cuando un bucle se puede hacer atómico.
Microsoft.Extensions.Caching
Se agrego soporte para IMemoryCache en la api prinicipal:
- MemoryCacheStatistics que contiene el tamaño de acierto/error/estimación de la caché y cuenta para IMemoryCache
- GetCurrentStatistics: devuelve una instancia de MemoryCacheStatistics, o null cuando TrackStatisticsMemoryCache el indicador no está habilitado. La biblioteca tiene una implementación incorporada disponible para.
GetCurrentStatistics() La API permite a los desarrolladores de aplicaciones usar contadores de eventos o API de métricas para realizar un seguimiento de las estadísticas de una o más memorias caché. Debemos tener presente que es posible usar estas API para obtener estadísticas para varias cachés, pero requiere que los desarrolladores escriban su propio medidor.
Ejemplo IMemoryCache.GetCurrentStatistics() para una memoria caché
// when using `services.AddMemoryCache(options => options.TrackStatistics = true);` to instantiate
[EventSource(Name = "Microsoft-Extensions-Caching-Memory")]
internal sealed class CachingEventSource : EventSource
{
public CachingEventSource(IMemoryCache memoryCache) { _memoryCache = memoryCache; }
protected override void OnEventCommand(EventCommandEventArgs command)
{
if (command.Command == EventCommand.Enable)
{
if (_cacheHitsCounter == null)
{
_cacheHitsCounter = new PollingCounter("cache-hits", this, () =>
_memoryCache.GetCurrentStatistics().CacheHits)
{
DisplayName = "Cache hits",
};
}
}
}
}
Ejemplo IMemoryCache.GetCurrentStatistics() para múltiples memorias caché
static Meter s_meter = new Meter("Microsoft.Extensions.Caching.Memory.MemoryCache", "1.0.0");
static IMemoryCache? mc1;
static IMemoryCache? mc2;
static void Main(string[] args)
{
s_meter.CreateObservableGauge<long>("cache-hits", GetCacheHits);
mc1 = new MemoryCache(new MemoryCacheOptions() { TrackStatistics = true, SizeLimit = 30 });
mc2 = new MemoryCache(new MemoryCacheOptions() { TrackStatistics = true, SizeLimit = 30 });
// call to: mc1.TryGetValue(key1, out object? value)
// or: mc2.TryGetValue(key2, out value2)
// increments TotalHits
}
// metrics callback for cache hits
static IEnumerable<Measurement<long>> GetCacheHits()
{
return new Measurement<long>[]
{
new Measurement<long>(mc1!.GetCurrentStatistics()!.TotalHits, new KeyValuePair<string,object?>("CacheName", "mc1")),
new Measurement<long>(mc2!.GetCurrentStatistics()!.TotalHits, new KeyValuePair<string,object?>("CacheName", "mc2")),
};
}
Soporte para achivos TAR
Estos archivos son muy comunes en Linux, tenemos un nuevo ensamblado llamado System.Formats.Tar, que contiene un soporte multiplataforma que permiten leer, escribir, archivar y extraer archivos Tar. Veamos los ejemplos:
Uso desde archivo
// Generates a tar archive where all the entry names are prefixed by the root directory 'SourceDirectory'
TarFile.CreateFromDirectory(sourceDirectoryName: "/home/dotnet/SourceDirectory/", destinationFileName: "/home/dotnet/destination.tar", includeBaseDirectory: true);
// Extracts the contents of a tar archive into the specified directory, but avoids overwriting anything found inside
TarFile.ExtractToDirectory(sourceFileName: "/home/dotnet/destination.tar", destinationDirectoryName: "/home/dotnet/DestinationDirectory/", overwriteFiles: false);
Desde un stream
// Generates a tar archive where all the entry names are prefixed by the root directory 'SourceDirectory'
using MemoryStream archiveStream = new();
TarFile.CreateFromDirectory(sourceDirectoryName: @"D:SourceDirectory", destination: archiveStream, includeBaseDirectory: true);
// Extracts the contents of a stream tar archive into the specified directory, and avoids overwriting anything found inside
TarFile.ExtractToDirectory(source: archiveStream, destinationDirectoryName: @"D:DestinationDirectory", overwriteFiles: false);
Usando métodos de compresión GZIP
using FileStream compressedStream = File.OpenRead("/home/dotnet/SourceDirectory/compressed.tar.gz");
using GZipStream decompressor = new(compressedStream, CompressionMode.Decompress);
TarFile.ExtractToDirectory(source: decompressor, destinationDirectoryName: "/home/dotnet/DestinationDirectory/", overwriteFiles: false);
Escribir entradas individuales en un archivo tar comprimido:
using MemoryStream archiveStream = new();
using (TarWriter writer = new TarWriter(archiveStream, TarFormat.Pax, leaveOpen: true))
{
// Add an entry from an existing file
writer.WriteEntry(fileName: @"D:SourceDirectoryfile.txt", entryName: "file.txt");
// Or write an entry from scratch
PaxTarEntry entry = new(entryType: TarEntryType.Directory, entryName: "directory");
writer.WriteEntry(entry);
}
using FileStream compressedStream = File.Create(@"D:DestinationDirectorycompressed.tar.gz");
using GZipStream compressor = new(compressedStream, CompressionMode.Compress);
archiveStream.Seek(0, SeekOrigin.Begin);
archiveStream.CopyTo(compressor);
CodeGen
Se real realizado muchos cambios por parte de la comunidad que han impactado en el rendimiento.
Se habilitó OSR de forma predeterminada en x64 y Arm64, y habilitó el jitting rápido para métodos con bucles en esas mismas plataformas. OSR permite que los métodos de ejecución prolongada cambien a versiones más optimizadas a mitad de la ejecución, por lo que el tiempo de ejecución puede jit todos los métodos rápidamente al principio y luego hacer la transición a versiones más optimizadas cuando esos métodos se llaman con frecuencia (a través de la compilación en niveles) o tienen bucles de ejecución prolongada (a través de OSR).
OSR mejora el tiempo de inicio. Algunos resultados dan una mejora del 25% en el tiempo de inicio en aplicaciones, y los diversos puntos de referencia de TechEmpower muestran mejoras del 10-30% en el tiempo hasta la primera solicitud.
Conclusiones
Tenemos varias mejoras de bajo nivel que están haciendo que la performance de .Net 7 sea cada vez más efectiva. En próximos posts veremos la preview 4 para ASP.Net Core y Entity Framework Core.