diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 85444a4..5cace4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,9 +61,6 @@ jobs: uses: endjin/Endjin.RecommendedPractices.GitHubActions/.github/workflows/scripted-build-matrix-pipeline.yml@main with: netSdkVersion: '10.x' - # Install the .NET 8 SDK too so the net8.0 targeting pack/runtime is available for - # building, testing and packing the multi-targeted (net8.0;net10.0) projects. - additionalNetSdkVersion: '8.x' # workflow_dispatch inputs are always strings, the type property is just for the UI forcePublish: ${{ github.event.inputs.forcePublish == 'true' }} skipCleanup: ${{ github.event.inputs.skipCleanup == 'true' }} @@ -78,7 +75,7 @@ jobs: testPhaseMatrixJson: | { "os": ["ubuntu-latest"], - "dotnetFramework": ["net8.0", "net10.0"] + "dotnetFramework": ["net10.0"] } # testArtifactName: '' # testArtifactPath: '' diff --git a/GitVersion.yml b/GitVersion.yml index 0a2f106..d3da6c6 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -20,4 +20,4 @@ branches: - feature - support - hotfix -next-version: "0.3" \ No newline at end of file +next-version: "1.0" \ No newline at end of file diff --git a/Solutions/Ais.Net.Models.Benchmarks/PositionBenchmarks.cs b/Solutions/Ais.Net.Models.Benchmarks/PositionBenchmarks.cs index e8ffaa1..3db16ca 100644 --- a/Solutions/Ais.Net.Models.Benchmarks/PositionBenchmarks.cs +++ b/Solutions/Ais.Net.Models.Benchmarks/PositionBenchmarks.cs @@ -5,9 +5,9 @@ namespace Ais.Net.Models.Benchmarks; using BenchmarkDotNet.Attributes; /// -/// Quantifies the cost of being a record (reference type). Every -/// decoded position is a heap allocation. The struct variant models the proposed -/// readonly record struct change to show the allocation saving on the decode path. +/// Confirms that constructing a is allocation-free. It was reworked from a +/// record (reference type, 32 B per instance) to a readonly record struct, so this +/// benchmark guards against a regression back to heap allocation. /// [MemoryDiagnoser] public class PositionBenchmarks @@ -15,12 +15,9 @@ public class PositionBenchmarks private const double Latitude = 50.12345; private const double Longitude = -1.98765; - [Benchmark(Baseline = true)] - public Position RecordClass() => new(Latitude, Longitude); - [Benchmark] - public PositionValue RecordStruct() => new(Latitude, Longitude); + public Position Construct() => new(Latitude, Longitude); - /// Models reworked as a value type. - public readonly record struct PositionValue(double Latitude, double Longitude); + [Benchmark] + public Position FromTenThousandthsOfMinutes() => Position.From10000thMins(30_000_000, 60_000_000); } diff --git a/Solutions/Ais.Net.Models.Benchmarks/VesselNameBenchmarks.cs b/Solutions/Ais.Net.Models.Benchmarks/VesselNameBenchmarks.cs index ee804f7..1820142 100644 --- a/Solutions/Ais.Net.Models.Benchmarks/VesselNameBenchmarks.cs +++ b/Solutions/Ais.Net.Models.Benchmarks/VesselNameBenchmarks.cs @@ -16,7 +16,7 @@ namespace Ais.Net.Models.Benchmarks; public class VesselNameBenchmarks { // A raw 20-character AIS name field: text plus '@' padding (the common case). - [Params("EVER GIVEN@@@@@@@@@@", "QUEEN MARY 2@@@@@", "@@@@@@@@@@@@@@@@@@@@")] + [Params("EVER GIVEN", "EVER GIVEN@@@@@@@@@@", "QUEEN MARY 2@@@@@", "@@@@@@@@@@@@@@@@@@@@")] public string RawName { get; set; } = string.Empty; // The previous implementation, kept as the baseline to show the improvement. diff --git a/Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj b/Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj index 8c17bd9..617ccee 100644 --- a/Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj +++ b/Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj @@ -2,7 +2,7 @@ - net8.0;net10.0 + net10.0 latest enable false diff --git a/Solutions/Ais.Net.Models.Specs/packages.lock.json b/Solutions/Ais.Net.Models.Specs/packages.lock.json index ba3248a..c3a54f2 100644 --- a/Solutions/Ais.Net.Models.Specs/packages.lock.json +++ b/Solutions/Ais.Net.Models.Specs/packages.lock.json @@ -180,215 +180,6 @@ "resolved": "0.4.2", "contentHash": "+wtx8g0YwxEaDbilf4MiED9S4Yxr9J7RHAAMEN48+yB0it10KYdhmtB6dxt7qdCOvdoDnBOumggcGYefWMz+WA==" } - }, - "net8.0": { - "Microsoft.Testing.Extensions.CodeCoverage": { - "type": "Direct", - "requested": "[18.9.0, )", - "resolved": "18.9.0", - "contentHash": "Odp8djWEUFTJoRV02y0zJaAwAHF31CtNaDgnKKM+8dBHbbaH7Ee0oo9gweLpbzjI7UrtMUzH6xLJSrICZw+Nwg==", - "dependencies": { - "Microsoft.DiaSymReader": "2.2.9", - "Microsoft.Extensions.DependencyModel": "10.0.8", - "Microsoft.Testing.Platform": "2.3.0", - "System.IO.Pipelines": "10.0.8", - "System.Text.Encodings.Web": "10.0.8", - "System.Text.Json": "10.0.8" - } - }, - "Microsoft.Testing.Extensions.TrxReport": { - "type": "Direct", - "requested": "[2.3.2, )", - "resolved": "2.3.2", - "contentHash": "JzCtiOAmIfkPP2PpAKZeiH1hVYIVC9jMhsPWrd6PnH/Wtuct1UMuxpKi2F1cmXgfokc/HOadC6HOO1qzI23tnA==", - "dependencies": { - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.2", - "Microsoft.Testing.Platform": "2.3.2" - } - }, - "MSTest.TestAdapter": { - "type": "Direct", - "requested": "[4.3.2, )", - "resolved": "4.3.2", - "contentHash": "uqpjJqQ3ws5z7Pq6BfMFpuqdrN2YIfe0Gfww91LRVXwxBdLqUaJd4JbHUImOSQvmK5m3PqW285kPjKbb69QFoA==", - "dependencies": { - "MSTest.TestFramework": "4.3.2", - "Microsoft.Testing.Extensions.VSTestBridge": "2.3.2", - "Microsoft.Testing.Platform.MSBuild": "2.3.2" - } - }, - "MSTest.TestFramework": { - "type": "Direct", - "requested": "[4.3.2, )", - "resolved": "4.3.2", - "contentHash": "Vlj3THt/BcOV/93k0Cdq29RNEsrY3TvUV6mF13rOuY8Gx7Ltd+LrOYId8AYu0cH9ioCYY/Sx1hppfI9p/vP+aw==", - "dependencies": { - "MSTest.Analyzers": "4.3.2" - } - }, - "NSubstitute": { - "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "0gvKMbiJ+/WrfbcfBfqRZZrvfLJcd3rqkqVMjjlY5dtmLRVzMY+o/K/rJUStofQ2haSr9Vd04YDfvZtVVGS3/A==", - "dependencies": { - "Castle.Core": "5.1.1" - } - }, - "Shouldly": { - "type": "Direct", - "requested": "[4.3.0, )", - "resolved": "4.3.0", - "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", - "dependencies": { - "DiffEngine": "11.3.0", - "EmptyFiles": "4.4.0" - } - }, - "Castle.Core": { - "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", - "dependencies": { - "System.Diagnostics.EventLog": "6.0.0" - } - }, - "DiffEngine": { - "type": "Transitive", - "resolved": "11.3.0", - "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", - "dependencies": { - "EmptyFiles": "4.4.0", - "System.Management": "6.0.1" - } - }, - "EmptyFiles": { - "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" - }, - "Microsoft.ApplicationInsights": { - "type": "Transitive", - "resolved": "2.23.0", - "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" - }, - "Microsoft.DiaSymReader": { - "type": "Transitive", - "resolved": "2.2.9", - "contentHash": "WLbDc/W9XkHpGVtd1L6Vml8W6+PaaYZZTJASHjHF7W2WX/hld6bfNK102Ui+I3a6zIdx4+oKRJtMgHz44JmBfw==" - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "10.0.8", - "contentHash": "vLyZVpxmduO2jx+76ggqnsA3m81kwMY3NkWciNTj5E+Nvqb0VihqCvQP89QsGONWp0AJwMZG+u9GzaCjDdFGNw==", - "dependencies": { - "System.Text.Encodings.Web": "10.0.8", - "System.Text.Json": "10.0.8" - } - }, - "Microsoft.Testing.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "2.3.2", - "contentHash": "Q1iapTk1DwFFT9aPtv4wxJHe22++NLxsg7IekEujac9RDAugxGQdCN0SQ1b9ryF78cdidhSBB17pHdCANurOyQ==", - "dependencies": { - "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "2.3.2" - } - }, - "Microsoft.Testing.Extensions.TrxReport.Abstractions": { - "type": "Transitive", - "resolved": "2.3.2", - "contentHash": "frw+Qa6gUxOZSw2Mw6SPtJT9gPzcP4nS9TfQhS0cHXKL6Rcp/CqLG+o4Psc1bHAhxW04rpzTBhjpNMnw2L7BuQ==", - "dependencies": { - "Microsoft.Testing.Platform": "2.3.2" - } - }, - "Microsoft.Testing.Extensions.VSTestBridge": { - "type": "Transitive", - "resolved": "2.3.2", - "contentHash": "DXJIaLlwt+6GnPurlf7eVMI0wAZV5KCZrgGPibhx9i9LNxAkwEOK2jVzdVjLvvZ/SX9MUUaGJB9yXuF/VoT4/w==", - "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "18.4.0", - "Microsoft.Testing.Extensions.Telemetry": "2.3.2", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.2", - "Microsoft.Testing.Platform": "2.3.2" - } - }, - "Microsoft.Testing.Platform": { - "type": "Transitive", - "resolved": "2.3.2", - "contentHash": "FuJgIEfNU+rFHJYGhuqA8uup+3cryJ1N67Tijqo01Jd5xAr28ptz0aXkFfSqK3MhAUmGsrPpgQ92+3+0oXv8AA==" - }, - "Microsoft.Testing.Platform.MSBuild": { - "type": "Transitive", - "resolved": "2.3.2", - "contentHash": "guFvvaE8FGUlB6h24jN+P+NBz/P5x4On9sk4n5UapUAPq4fr0y1AgUe4sXmItp7r2NMe3rhZYLE1teP9T9sVbg==", - "dependencies": { - "Microsoft.Testing.Platform": "2.3.2" - } - }, - "Microsoft.TestPlatform.ObjectModel": { - "type": "Transitive", - "resolved": "18.4.0", - "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg==" - }, - "MSTest.Analyzers": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "GNvrVI6/Y5pNoKnK1/VELBls3XiHNZaj+zh14LtwlCqFI8zXTW3vuBe0Jv2qEr9wxHlOAQQBTJPRoNRjneGrKQ==" - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, - "System.Diagnostics.EventLog": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "10.0.8", - "contentHash": "STVNTIVd+UrYvSo31D2tOOTs19IThdjGDN14FS3/NZb4PWsVakAg/VT4sq+JYWGP/GtazzvXwebWL6LPBAwnFQ==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", - "dependencies": { - "System.CodeDom": "6.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "10.0.8", - "contentHash": "HvFIJXM/CMTRu6PBQmjukQZ/O32Vx5fOEYZs0kq0OD5s9vJQ19KHLWCVmnfh3gNC7pbYomm06tzOTgJBflr/nQ==" - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "10.0.8", - "contentHash": "/fysUDkD7oFGaRPoA7IaFs0wRoO3GlwlCNq2P+xWZqxLy1R4cktRSKfMjJDy9ymS4grL7IDVdt8de8L9a0z55Q==", - "dependencies": { - "System.IO.Pipelines": "10.0.8", - "System.Text.Encodings.Web": "10.0.8" - } - }, - "ais.net.models": { - "type": "Project", - "dependencies": { - "Ais.Net": "[0.4.2, )" - } - }, - "Ais.Net": { - "type": "CentralTransitive", - "requested": "[0.4.2, )", - "resolved": "0.4.2", - "contentHash": "+wtx8g0YwxEaDbilf4MiED9S4Yxr9J7RHAAMEN48+yB0it10KYdhmtB6dxt7qdCOvdoDnBOumggcGYefWMz+WA==", - "dependencies": { - "System.IO.Pipelines": "4.7.4" - } - } } } } \ No newline at end of file diff --git a/Solutions/Ais.Net.Models/Ais.Net.Models.csproj b/Solutions/Ais.Net.Models/Ais.Net.Models.csproj index a0e40c7..1b7fb4c 100644 --- a/Solutions/Ais.Net.Models/Ais.Net.Models.csproj +++ b/Solutions/Ais.Net.Models/Ais.Net.Models.csproj @@ -3,7 +3,7 @@ - net8.0;net10.0 + net10.0 latest enable True @@ -15,7 +15,14 @@ Apache-2.0 .NET types and interfaces that describe AIS message types 1,2,3,5,18,19,24 Part 0, and 24 Part 1. These can be used on top of the high performance, zero allocation AIS.Net message decoder, which can process millions of AIVDM/AIVDO sentences per second on a single core. Sponsored by endjin. ais;aisvdm;aivdo;nmea;marine;gis;endjin - + + Major release. + BREAKING: now targets .NET 10 only (previous releases supported .NET 8). + BREAKING: Position is now a readonly record struct (value type) instead of a class, eliminating a heap allocation per decoded position. + Fixed: ToShipTypeGroup(49) now returns HighSpeedCraftNoAdditionalInformation; CleanVesselName correctly strips '@'/space padding and collapses interior spaces. + Performance: allocation-free vessel-name cleaning and ship-type classification. + Added XML documentation across the public API. + diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/Position.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/Position.cs index edb6cf6..a3986f7 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/Position.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/Abstractions/Position.cs @@ -9,7 +9,11 @@ namespace Ais.Net.Models.Abstractions; /// /// The latitude in degrees (positive north of the equator). /// The longitude in degrees (positive east of the prime meridian). -public record Position(double Latitude, double Longitude) +/// +/// This is a value type so that a decoded position incurs no heap allocation; on the AIS decode +/// path this avoids one allocation per positional message. +/// +public readonly record struct Position(double Latitude, double Longitude) { /// /// Creates a from coordinates expressed in ten-thousandths of a minute diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageExtensions.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageExtensions.cs index 0d1f5a8..507baf1 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageExtensions.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageExtensions.cs @@ -77,6 +77,13 @@ public static string CleanVesselName(this string value) return string.Empty; } + // Fast path: if nothing needed trimming and there are no interior double-spaces to + // collapse, the value is already clean - return it as-is and allocate nothing. + if (trimmed.Length == value.Length && trimmed.IndexOf(" ") < 0) + { + return value; + } + // AIS text fields are short (<= 20 chars); the heap fallback guards against a pathological // caller passing an over-long string to what is otherwise a stackalloc. Span buffer = trimmed.Length <= 256 ? stackalloc char[256] : new char[trimmed.Length]; diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType18.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType18.cs index 9d32279..8859236 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType18.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType18.cs @@ -27,7 +27,7 @@ namespace Ais.Net.Models; /// The speed over ground in knots, or null when not available. /// The UTC second when the report was generated. /// The true heading in degrees. -public record AisMessageType18( +public sealed record AisMessageType18( bool CanAcceptMessage22ChannelAssignment, bool CanSwitchBands, float? CourseOverGround, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType19.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType19.cs index 587acf4..de343e7 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType19.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType19.cs @@ -30,7 +30,7 @@ namespace Ais.Net.Models; /// The speed over ground in knots, or null when not available. /// The UTC second when the report was generated. /// The true heading in degrees. -public record AisMessageType19( +public sealed record AisMessageType19( float? CourseOverGround, uint DimensionToBow, uint DimensionToPort, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType1Through3.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType1Through3.cs index f7bcb82..f8ad5fc 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType1Through3.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType1Through3.cs @@ -26,7 +26,7 @@ namespace Ais.Net.Models; /// The speed over ground in knots, or null when not available. /// The UTC second when the report was generated. /// The true heading in degrees. -public record AisMessageType1Through3( +public sealed record AisMessageType1Through3( float? CourseOverGround, ManoeuvreIndicator ManoeuvreIndicator, int MessageType, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part0.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part0.cs index 622f239..c15b512 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part0.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part0.cs @@ -13,7 +13,7 @@ namespace Ais.Net.Models; /// The part number identifying this as Part A of the static data report. /// The number of times the message has been repeated. /// The value of the spare bits at bit offset 160. -public record AisMessageType24Part0( +public sealed record AisMessageType24Part0( uint Mmsi, uint PartNumber, uint RepeatIndicator, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part1.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part1.cs index ce2d5ac..7fda2c1 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part1.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType24Part1.cs @@ -24,7 +24,7 @@ namespace Ais.Net.Models; /// The unit model code of the Class B equipment. /// The vendor identifier as encoded under revision 3 of the specification. /// The vendor identifier as encoded under revision 4 of the specification. -public record AisMessageType24Part1( +public sealed record AisMessageType24Part1( string CallSign, uint DimensionToBow, uint DimensionToPort, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType27.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType27.cs index 0cfe8e0..f4bc897 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType27.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType27.cs @@ -18,7 +18,7 @@ namespace Ais.Net.Models; /// Whether Receiver Autonomous Integrity Monitoring is in use. /// The number of times the message has been repeated. /// The speed over ground in knots, or null when not available. -public record AisMessageType27( +public sealed record AisMessageType27( float? CourseOverGround, bool GnssPositionStatus, uint Mmsi, diff --git a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType5.cs b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType5.cs index eb53894..c160825 100644 --- a/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType5.cs +++ b/Solutions/Ais.Net.Models/Ais/Net/Models/AisMessageType5.cs @@ -29,7 +29,7 @@ namespace Ais.Net.Models; /// The ship-and-cargo type code. /// The value of the spare bits at bit offset 423. /// The name of the vessel. -public record AisMessageType5( +public sealed record AisMessageType5( uint AisVersion, string CallSign, string Destination,