Skip to content

Conversation

@PrzemyslawKlys
Copy link
Contributor

Here's a second version after updates that required some changes to tests as the differences of semantic JSON parsing


BenchmarkDotNet v0.15.2, Windows 11 (10.0.26100.4652/24H2/2024Update/HudsonValley)
Unknown processor
.NET SDK 9.0.304
  [Host]     : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
  DefaultJob : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI


Method Mean Error StdDev Gen0 Gen1 Allocated
STJ_String_Small 283.3 ns 3.32 ns 3.10 ns 0.0448 - 752 B
STJ_Bytes_Small 243.5 ns 2.57 ns 2.15 ns 0.0448 - 752 B
STJ_String_Medium 503.9 ns 4.85 ns 4.05 ns 0.2584 0.0038 4336 B
STJ_Bytes_Medium 410.1 ns 7.92 ns 8.13 ns 0.2589 0.0038 4336 B
STJ_String_Large 3,994.9 ns 38.03 ns 35.57 ns 3.9215 0.7782 65776 B
STJ_Bytes_Large 2,881.5 ns 9.88 ns 7.71 ns 3.9215 0.7820 65776 B

Benchmark Comparison – Original vs New vs Final

⏱️ Throughput (ns)

Scenario Original New (pre-latest) Final Δ New vs Orig Δ Final vs Orig Δ Final vs New
STJ_String_Small 279.8 ns 287.1 ns 283.3 ns −2.6% −1.2% +1.3%
STJ_Bytes_Small 342.9 ns 247.2 ns 243.5 ns +27.9% +29.0% +1.5%
STJ_String_Medium 631.9 ns 499.8 ns 503.9 ns +20.9% +20.3% −0.8%
STJ_Bytes_Medium 777.3 ns 512.3 ns 410.1 ns +34.1% +47.2% +20.0%
STJ_String_Large 4,043.0 ns 4,610.5 ns 3,994.9 ns −14.0% +1.2% +13.4%
STJ_Bytes_Large 36,013.7 ns 3,421.0 ns 2,881.5 ns +90.5% +92.0% +15.8%
  • Δ columns are speedup vs the left baseline (positive = faster, negative = slower).
  • Bold values indicate the best performer.

🧠 Allocations

Scenario Original New (pre-latest) Final
STJ_String_Small 752 B 752 B 752 B
STJ_Bytes_Small 1,784 B 752 B 752 B
STJ_String_Medium 4,336 B 4,336 B 4,336 B
STJ_Bytes_Medium 12,536 B 4,336 B 4,336 B
STJ_String_Large 65,776 B 65,776 B 65,776 B
STJ_Bytes_Large 196,884 B 65,776 B 65,776 B

Reduction vs Original (Final):

  • Bytes-Small: −57.8%
  • Bytes-Medium: −65.4%
  • Bytes-Large: −66.6%

✅ Key Takeaways

  • Final version dominates on byte input (the hot path in AD/LAPS):
    • ~92% faster for large payloads.
    • ~2/3 fewer allocations.
  • String input regressions fixed or minimized:
    • Large strings are now faster than original (~+1%).
    • Medium strings still ~20% faster than original (slightly slower than pre-latest, but within noise).
    • Small strings within ~1% of original.

🚀 Final

  • UTF-8 span fast path.
  • UTF-16 decode to string.
  • Lenient reader vs strict writer split.
  • Bounded single-quote heuristic.

…lable types

* Changed return type of `DeserializeLenient<T>` methods to `T?` for better null handling.
* Improved handling of JSON input by adding checks for empty or whitespace strings.
* Enhanced UTF-8 and UTF-16 decoding paths for more robust JSON processing.
* Added helper methods to streamline BOM trimming and single-quote detection.
* Introduced `JsonAssert.AreEqual` method to canonicalize and compare JSON strings.
* Updated tests to utilize `JsonAssert` for improved readability and maintainability.
* Refactored `DsiJson` to include separate options for reading and writing JSON.
* Added `DsiJsonContext` for source-generated serialization options.
@MichaelGrafnetter
Copy link
Owner

I was about to release a new version, but your previous PR has broken dependencies, requiring a newer version of System.Runtime.CompilerServices.Unsafe. It does not get loaded into PowerShell. I will either need to quickly figure out how to do assembly binding redirects or do a rollback to Newtonsoft. 😒

@PrzemyslawKlys
Copy link
Contributor Author

PrzemyslawKlys commented Aug 17, 2025

using System;
using System.IO;
using System.Management.Automation;
using System.Reflection;
using System.Collections.Generic;

/// <summary>
/// OnModuleImportAndRemove is a class that implements the IModuleAssemblyInitializer and IModuleAssemblyCleanup interfaces.
/// This class is used to handle the assembly resolve event when the module is imported and removed.
/// </summary>
public class OnModuleImportAndRemove : IModuleAssemblyInitializer, IModuleAssemblyCleanup {
    /// <summary>
    /// OnImport is called when the module is imported.
    /// </summary>
    public void OnImport() {
        if (IsNetFramework()) {
            AppDomain.CurrentDomain.AssemblyResolve += MyResolveEventHandler;
        }
    }

    /// <summary>
    /// Called when the module is removed from the PowerShell session.
    /// </summary>
    /// <param name="module">Module being removed.</param>
    public void OnRemove(PSModuleInfo module) {
        if (IsNetFramework()) {
            AppDomain.CurrentDomain.AssemblyResolve -= MyResolveEventHandler;
        }
    }

    /// <summary>
    /// Handles assembly resolution for dependencies shipped with the module.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="args">Information about the assembly to resolve.</param>
    /// <returns>The loaded assembly or <c>null</c> if not found.</returns>
    private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args) {
        var libDirectory = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location);
        var directoriesToSearch = new List<string> { libDirectory };

        if (Directory.Exists(libDirectory)) {
            directoriesToSearch.AddRange(Directory.GetDirectories(libDirectory, "*", SearchOption.AllDirectories));
        }

        var requestedAssemblyName = new AssemblyName(args.Name).Name + ".dll";

        foreach (var directory in directoriesToSearch) {
            var assemblyPath = Path.Combine(directory, requestedAssemblyName);

            if (File.Exists(assemblyPath)) {
                try {
                    return Assembly.LoadFrom(assemblyPath);
                } catch (Exception ex) {
                    Console.WriteLine($"Failed to load assembly from {assemblyPath}: {ex.Message}");
                }
            }
        }

        return null;
    }

    /// <summary>
    /// Determines whether the current runtime is .NET Framework.
    /// </summary>
    /// <returns><c>true</c> if running on .NET Framework; otherwise <c>false</c>.</returns>
    private bool IsNetFramework() {
        return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Determines whether the current runtime is .NET Core.
    /// </summary>
    /// <returns><c>true</c> if running on .NET Core; otherwise <c>false</c>.</returns>
    private bool IsNetCore() {
        return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET Core", StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Determines whether the current runtime is .NET 5 or higher.
    /// </summary>
    /// <returns><c>true</c> if running on .NET 5 or newer; otherwise <c>false</c>.</returns>
    private bool IsNet5OrHigher() {
        return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET 5", StringComparison.OrdinalIgnoreCase) ||
               System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET 6", StringComparison.OrdinalIgnoreCase) ||
               System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET 7", StringComparison.OrdinalIgnoreCase) ||
               System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET 8", StringComparison.OrdinalIgnoreCase) ||
               System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET 9", StringComparison.OrdinalIgnoreCase);
    }
}

try putting this in PowerShell module - this is my drop-in for assembly issues and conflicts resolver for PS 5.1, it works for me 99% of times when i deal with dependency problems and conflicts. For PS 7 there's ALC, but that's probably non-issue in PS 7 for now, but for reference: https://github.com/jborean93/PowerShell-ALC

Alternatively:

For PS 5.1, but that would probably require some JSON changes, and not really great idea.

Another idea would be to use Newtonsoft for PS 5.1 as it's pretty much used by PowerShell itself, and use STJ in PS 7+

@MichaelGrafnetter
Copy link
Owner

I "jiggled" a little with dependency versions and it just started to work. But I am afraid that sooner or later, I will need to use the IModuleAssemblyInitializer code you posted, so thanks for that.

@PrzemyslawKlys
Copy link
Contributor Author

I use this code in like 30 of my modules that have heavy dependency issues and conflicts. Never fails for PS 5.1 :-) Saves my ass every time and some of my projects have really like 10 dependencies, different projects even within my project sometimes conflicting. PS is dependency mess. And then other people release their own versions or like Microsoft - Microsoft.Identity.dll is in 3-4 different versions Exchange, Graph, Azure, has each their own version and of course it conflicts out ;)

So the code is pretty safe to use.

@MichaelGrafnetter
Copy link
Owner

Yes. Every time I want to demo Microsoft Graph PowerShell to someone, I end up with some assemblies not loading and having to update all modules. So even MSFT is unable to solve this. Do you remember when .NET Framework was introduced 20+ years ago? One of the key features was "solving DLL hell".

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.

2 participants