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
5 changes: 1 addition & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand All @@ -78,7 +75,7 @@ jobs:
testPhaseMatrixJson: |
{
"os": ["ubuntu-latest"],
"dotnetFramework": ["net8.0", "net10.0"]
"dotnetFramework": ["net10.0"]
}
# testArtifactName: ''
# testArtifactPath: ''
Expand Down
2 changes: 1 addition & 1 deletion GitVersion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ branches:
- feature
- support
- hotfix
next-version: "0.3"
next-version: "1.0"
15 changes: 6 additions & 9 deletions Solutions/Ais.Net.Models.Benchmarks/PositionBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,19 @@ namespace Ais.Net.Models.Benchmarks;
using BenchmarkDotNet.Attributes;

/// <summary>
/// Quantifies the cost of <see cref="Position"/> being a <c>record</c> (reference type). Every
/// decoded position is a heap allocation. The struct variant models the proposed
/// <c>readonly record struct</c> change to show the allocation saving on the decode path.
/// Confirms that constructing a <see cref="Position"/> is allocation-free. It was reworked from a
/// <c>record</c> (reference type, 32 B per instance) to a <c>readonly record struct</c>, so this
/// benchmark guards against a regression back to heap allocation.
/// </summary>
[MemoryDiagnoser]
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);

/// <summary>Models <see cref="Position"/> reworked as a value type.</summary>
public readonly record struct PositionValue(double Latitude, double Longitude);
[Benchmark]
public Position FromTenThousandthsOfMinutes() => Position.From10000thMins(30_000_000, 60_000_000);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Solutions/Ais.Net.Models.Specs/Ais.Net.Models.Specs.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project Sdk="MSTest.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
Expand Down
209 changes: 0 additions & 209 deletions Solutions/Ais.Net.Models.Specs/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
11 changes: 9 additions & 2 deletions Solutions/Ais.Net.Models/Ais.Net.Models.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="$(EndjinProjectPropsPath)" Condition="$(EndjinProjectPropsPath) != ''" />

<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EndjinDisableCodeAnalysis>True</EndjinDisableCodeAnalysis>
Expand All @@ -15,7 +15,14 @@
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageDescription>.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.</PackageDescription>
<PackageTags>ais;aisvdm;aivdo;nmea;marine;gis;endjin</PackageTags>
<PackageReleaseNotes></PackageReleaseNotes>
<PackageReleaseNotes>
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.
</PackageReleaseNotes>
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ namespace Ais.Net.Models.Abstractions;
/// </summary>
/// <param name="Latitude">The latitude in degrees (positive north of the equator).</param>
/// <param name="Longitude">The longitude in degrees (positive east of the prime meridian).</param>
public record Position(double Latitude, double Longitude)
/// <remarks>
/// 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.
/// </remarks>
public readonly record struct Position(double Latitude, double Longitude)
{
/// <summary>
/// Creates a <see cref="Position"/> from coordinates expressed in ten-thousandths of a minute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<char> buffer = trimmed.Length <= 256 ? stackalloc char[256] : new char[trimmed.Length];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace Ais.Net.Models;
/// <param name="SpeedOverGround">The speed over ground in knots, or null when not available.</param>
/// <param name="TimeStampSecond">The UTC second when the report was generated.</param>
/// <param name="TrueHeadingDegrees">The true heading in degrees.</param>
public record AisMessageType18(
public sealed record AisMessageType18(
bool CanAcceptMessage22ChannelAssignment,
bool CanSwitchBands,
float? CourseOverGround,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ namespace Ais.Net.Models;
/// <param name="SpeedOverGround">The speed over ground in knots, or null when not available.</param>
/// <param name="TimeStampSecond">The UTC second when the report was generated.</param>
/// <param name="TrueHeadingDegrees">The true heading in degrees.</param>
public record AisMessageType19(
public sealed record AisMessageType19(
float? CourseOverGround,
uint DimensionToBow,
uint DimensionToPort,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace Ais.Net.Models;
/// <param name="SpeedOverGround">The speed over ground in knots, or null when not available.</param>
/// <param name="TimeStampSecond">The UTC second when the report was generated.</param>
/// <param name="TrueHeadingDegrees">The true heading in degrees.</param>
public record AisMessageType1Through3(
public sealed record AisMessageType1Through3(
float? CourseOverGround,
ManoeuvreIndicator ManoeuvreIndicator,
int MessageType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Ais.Net.Models;
/// <param name="PartNumber">The part number identifying this as Part A of the static data report.</param>
/// <param name="RepeatIndicator">The number of times the message has been repeated.</param>
/// <param name="Spare160">The value of the spare bits at bit offset 160.</param>
public record AisMessageType24Part0(
public sealed record AisMessageType24Part0(
uint Mmsi,
uint PartNumber,
uint RepeatIndicator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace Ais.Net.Models;
/// <param name="UnitModelCode">The unit model code of the Class B equipment.</param>
/// <param name="VendorIdRev3">The vendor identifier as encoded under revision 3 of the specification.</param>
/// <param name="VendorIdRev4">The vendor identifier as encoded under revision 4 of the specification.</param>
public record AisMessageType24Part1(
public sealed record AisMessageType24Part1(
string CallSign,
uint DimensionToBow,
uint DimensionToPort,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Ais.Net.Models;
/// <param name="RaimFlag">Whether Receiver Autonomous Integrity Monitoring is in use.</param>
/// <param name="RepeatIndicator">The number of times the message has been repeated.</param>
/// <param name="SpeedOverGround">The speed over ground in knots, or null when not available.</param>
public record AisMessageType27(
public sealed record AisMessageType27(
float? CourseOverGround,
bool GnssPositionStatus,
uint Mmsi,
Expand Down
Loading
Loading