Skip to content

feat!: Switch out Newtonsoft for STJ#468

Open
JR-Morgan wants to merge 17 commits intomainfrom
jrm/stj-experiment
Open

feat!: Switch out Newtonsoft for STJ#468
JR-Morgan wants to merge 17 commits intomainfrom
jrm/stj-experiment

Conversation

@JR-Morgan
Copy link
Copy Markdown
Member

@JR-Morgan JR-Morgan commented Apr 7, 2026

This is a semi-experimental change to replace newtonsoft with System.Text.Json for the new send pipeline serializer


Newtonsoft has been largely stale for the past 5 years; and System.Text.Json has exceeded it's performance considerably. For us, this mostly comes down being able to utilize modern .NET features to reduce memory allocations.

Historically, we've have been hesitant to adopt STJ because

  1. Our serializer has been historically, very complex, coupling us to the same solutions.
  2. We've not been building apps that run under K8s, where OOM conditions affect our reliability.
  3. We've not had many .NET5+ products that would have greatly benefited
  4. We've been (with good reason) adverse to DLL conflicts in .NET framework apps

But for the following reasons, I think we're now in a good position to consider the switch

  1. The new ndjson based serializer is far simpler now we've removed the need for chunking and dynamic detaching.
  2. DLL conflicts less of a concern, now many apps are on .NET8+ which includes STJ out of the box.

Serializer Benchmarks - https://app.speckle.systems/projects/bf5b49215c/models/0d573b9a34

NET 10

Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
PR 3.127 s 1.808 s 0.2798 s 61000.0000 17000.0000 1000.0000 1.85 GB
3.16.0 5.892 s 3.722 s 0.5760 s 234000.0000 20000.0000 5.19 GB

NET 8.0

Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
PR 3.350 s 3.345 s 0.5176 s 61000.0000 19000.0000 1000.0000 1.85 GB
3.16.0 7.593 s 2.206 s 0.3414 s 234000.0000 20000.0000 5.19 GB

NET 4.8

Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
PR 9.098 s 2.663 s 0.4122 s 588000.0000 30000.0000 1000.0000 4.39 GB
3.16.0 26.44 s 2.746 s 0.425 s 693000.0000 33000.0000 6.43 GB

TODO:

  • re)Validate performance DONE!
    • I'm expecting the .NET8 and .NET 10 targets to be most benefited.
    • Memory allocations should be measurably lower.
    • Raw speed should also be improved too
    • .netstandard2.0 less so (I'm not expecting any regressions, but we should test)
  • DLL conflicts
    • for .netstandard2.0 we've introduced a dependency on STJ. Lets determine if this will be a problem. we may be able work around it with ILRepack, or by lowering the version requirement.
  • Test coverage
    • Ensure test coverage of the simple stuff (id generation etc...)
  • IL repack System.Memory? or no? ❎Can't since we need to expose the IBufferWriter interface from Speckle.Sdk.Depedencies

@JR-Morgan JR-Morgan marked this pull request as draft April 7, 2026 09:35
@JR-Morgan JR-Morgan changed the title experiment: STJ feat!: Switch out Newtonsoft for STJ Apr 7, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 54.12371% with 89 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.97%. Comparing base (55082df) to head (1ad76fe).

Files with missing lines Patch % Lines
src/Speckle.Sdk/Pipelines/Send/Serializer.cs 32.22% 61 Missing ⚠️
src/Speckle.Sdk/Pipelines/Send/DiskStore.cs 0.00% 13 Missing ⚠️
...nes/Send/Serialization/JsonIgnoreAttributeTests.cs 89.36% 5 Missing ⚠️
...ependencies/ArrayBufferWriterPooledObjectPolicy.cs 42.85% 4 Missing ⚠️
src/Speckle.Sdk/Pipelines/Send/EfficientJson.cs 57.14% 3 Missing ⚠️
src/Speckle.Sdk/Models/Attributes.cs 75.00% 1 Missing ⚠️
src/Speckle.Sdk/Pipelines/Send/SendPipeline.cs 0.00% 1 Missing ⚠️
src/Speckle.Sdk/Pipelines/Send/UploaderDTOs.cs 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #468      +/-   ##
==========================================
+ Coverage   71.42%   71.97%   +0.55%     
==========================================
  Files         353      358       +5     
  Lines       14332    14425      +93     
  Branches     1209     1213       +4     
==========================================
+ Hits        10237    10383     +146     
+ Misses       3698     3637      -61     
- Partials      397      405       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JR-Morgan JR-Morgan force-pushed the jrm/stj-experiment branch from 4558f3e to 72f6898 Compare April 17, 2026 08:59
@JR-Morgan JR-Morgan force-pushed the jrm/stj-experiment branch from ea0a9ed to 87dc3ac Compare April 17, 2026 11:00
@JR-Morgan JR-Morgan deployed to nuget.org April 17, 2026 14:22 — with GitHub Actions Active
@JR-Morgan JR-Morgan marked this pull request as ready for review April 17, 2026 14:33
/// <summary>
/// Represents a heap-based, array-backed output sink into which <typeparam name="T"/> data can be written.
/// </summary>
public sealed class ArrayBufferWriter<T> : IBufferWriter<T>
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Like how before, we were pooling StringBuilders as the buffer behind the newtonsoft json writer.
The STJ equivalent is these ArrayBufferWriter

We're pooling them in the same way

{
using var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
using var gzip = new GZipStream(fileStream, CompressionLevel.Optimal);
using var writer = new StreamWriter(gzip);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Since the ArrayBufferWriter<byte> is buffering bytes not chars, we don't need to use a StreamWriter anymore, we can directly write to the gzip stream

using var jsonWriter = new JsonTextWriter(stringWriter);
using var idWriter = new SerializerIdWriter(jsonWriter);
var efficientJson = new EfficientJson();
using var jsonWriter = new Utf8JsonWriter(efficientJson.Buffer);
Copy link
Copy Markdown
Member Author

@JR-Morgan JR-Morgan Apr 17, 2026

Choose a reason for hiding this comment

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

This Utf8JsonWriter is the "start" of our journy into STJ

Utf8JsonWriters are backed by an ArrayBufferWriter (which we're pooling in the similar way we were previously pooling string buildres)

#if NET6_0_OR_GREATER
string id = IdGenerator.ComputeId(efficientJson.WrittenSpan);
#else
string id = IdGenerator.ComputeId(efficientJson.GetInternalBuffer(), 0, efficientJson.WrittenCount);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Since the ArrayBufferWriter gives us "low level" access to its buffer, we can avoid having a seprate id writer.

// Will throw JsonWriterException if not supported
writer.WriteValue(value);
return;
throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported type {value.GetType()}");
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Newtonsoft gave a handy WriteValue(object) overload which supported a bunch of primitives, and would throw if not supported.

STJ doesn't have an equivalent, so we've added cases for all the primitives higher up in this switch statement. Then this will throw like before if not supported.

@JR-Morgan JR-Morgan marked this pull request as draft April 17, 2026 16:40
@linear
Copy link
Copy Markdown

linear bot commented Apr 20, 2026

@JR-Morgan JR-Morgan marked this pull request as ready for review April 21, 2026 09:10
@JR-Morgan JR-Morgan enabled auto-merge (squash) April 21, 2026 10:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant