-1

Initial memory usage was 4660k, and then increased to 6920k, however, it did not decrease in the end.

demo

static void Main(string[] args)
{
    string data = File.ReadAllText("./generated.json");
    Console.WriteLine("Begin parsing data...");
    for (var i = 0; i < 100; i++)
    {
        using (JsonDocument jsonDocument = JsonDocument.Parse(data))
        {
        }
        Thread.Sleep(650);
    }
    Console.WriteLine("Batch task ends...");
    GC.Collect();
    Console.ReadLine();
}

here is my generated.json

pigLoveRabbit520
  • 513
  • 7
  • 19
  • Measured how? There's no code that actually measures the various memory consumption types or GC generations. If you want usable numbers use BenchmarkDotNet with the [memory diagnosers](https://benchmarkdotnet.org/articles/configs/diagnosers.html) to see what's actually being allocated – Panagiotis Kanavos Nov 21 '22 at 07:59
  • Did you run the code in Debug or Release? – Matteo Umili Nov 21 '22 at 08:01
  • Besides, disposing an object doesn't mean deleting it or its buffers. Especially if those buffers are shared and reused. System.Text.Json uses pooled buffers extensively, so I'd guess `Dispose()` releases the internal buffers back to the pool so they can be reused. Instead of allocating 100 buffers, the code you posted allocates just 1 and reuses it. That buffer isn't orphaned at the end, so it's not GCd – Panagiotis Kanavos Nov 21 '22 at 08:04
  • @MatteoUmili in Debug. – pigLoveRabbit520 Nov 21 '22 at 08:05
  • .NET is open source and the code for [Dispose](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs#LL61C12-L61C12) shows that indeed, `Dispose()` releases the rented buffers. There's no leak, quite the opposite – Panagiotis Kanavos Nov 21 '22 at 08:05
  • How big is the source file? – Panagiotis Kanavos Nov 21 '22 at 08:06
  • @pigLoveRabbit520 How about trying in Release? – Matteo Umili Nov 21 '22 at 08:19
  • You still haven't included any benchmark code or measurements. There's no indication that there's a leak – Panagiotis Kanavos Nov 21 '22 at 08:23
  • Check [this question that uses BencharkDotNet](https://stackoverflow.com/questions/74515573/why-is-the-enumerable-anyfunctsource-bool-predicate-slow-compared-to-a-fore) to see what needs to be posted. In your case you'll have to include the memory and GC columns. You can also use a profiler, eg Visual Studio's or Rider's profiler, to see how memory is used. – Panagiotis Kanavos Nov 21 '22 at 08:27
  • Even with just Visual Studio, the [Diagnostics Tools Window](https://learn.microsoft.com/en-us/visualstudio/profiling/running-profiling-tools-with-or-without-the-debugger?view=vs-2022#BKMK_Quick_start__Collect_diagnostic_data) will display a memory usage graph. If there's a leak, you'll see a steadily increasing graph or a sawline, as temporary objects get allocated and then GCd. You can use the Memory Snapshot tool to see the memory difference before and after, and what new objects were created – Panagiotis Kanavos Nov 21 '22 at 08:29

1 Answers1

1

If there was a leak, every .NET Core developer would have noticed because System.Text.Json is central to ASP.NET Core. In fact, the question's code reduces memory consumption by 99 times.

The classes in the System.Text.Json namespace were built to reduce allocations as much as possible, not only to reduce memory consumption but to increase speed as well. Allocating and garbage-collecting buffers is expensive, especially for large buffers. It's better to reuse a buffer than delete it only to create a new similar one for the next call.

One of the ways they do that is by using pooled buffers instead of allocating a new one every time. JsonDocument's Dispose releases the shared buffers it used, so they can be reused :

    public void Dispose()
    {
        int length = _utf8Json.Length;
        if (length == 0 || !IsDisposable)
        {
            return;
        }

        _parsedData.Dispose();
        _utf8Json = ReadOnlyMemory<byte>.Empty;

        if (_extraRentedArrayPoolBytes != null)
        {
            byte[]? extraRentedBytes = Interlocked.Exchange<byte[]?>(ref _extraRentedArrayPoolBytes, null);

            if (extraRentedBytes != null)
            {
                // When "extra rented bytes exist" it contains the document,
                // and thus needs to be cleared before being returned.
                extraRentedBytes.AsSpan(0, length).Clear();
                ArrayPool<byte>.Shared.Return(extraRentedBytes);
            }
        }
        else if (_extraPooledByteBufferWriter != null)
        {
            PooledByteBufferWriter? extraBufferWriter = Interlocked.Exchange<PooledByteBufferWriter?>(ref _extraPooledByteBufferWriter, null);
            extraBufferWriter?.Dispose();
        }
    }

All calls are involved in returning pooled buffers and objects back to the pool. Even _parsedData.Dispose() eventually calls ArrayPool.Shared.Return(data)

        public void Dispose()
        {
            byte[]? data = Interlocked.Exchange(ref _data, null!);
            if (data == null)
            {
                return;
            }

            Debug.Assert(!_isLocked, "Dispose called on a locked database");

            // The data in this rented buffer only conveys the positions and
            // lengths of tokens in a document, but no content; so it does not
            // need to be cleared.
            ArrayPool<byte>.Shared.Return(data);
            Length = 0;
        }
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • I have a service that will receive thousands of json messages one day, I just worry it will use all memory of the machine. – pigLoveRabbit520 Nov 21 '22 at 08:45
  • Why do you assume there's a leak in the first place? Your code *could* flood the machine if JsonDocument behaved the way you expected - keeping orphaned buffers in memory until they get GCd and creating new once. Using pooled buffers is how this is avoided in the first place, in all languages, including lower-level ones like C++ and C. – Panagiotis Kanavos Nov 21 '22 at 08:49
  • If you want to see how your application behaves use a profiler or BenchmarkDotNet. Don't just assume. String manipulations in your service will cause far more memory usage than JSON parsing, because strings are immutable so any modification creates a new string that needs to be GCd at the end – Panagiotis Kanavos Nov 21 '22 at 08:52