File: Writing\BatchingLsifJsonWriter.cs
Web Access
Project: ..\..\..\src\Features\Lsif\Generator\Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.csproj (Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Generic;
using Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Graph;
 
namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Writing
{
    /// <summary>
    /// An <see cref="ILsifJsonWriter"/> that lets us batch up a bunch of elements and
    /// write them at once. This allows for less contention on shared locks since we can have
    /// multiple parallel tasks producing items, and then only at the end contribute items into
    /// the final file.
    /// </summary>
    internal sealed class BatchingLsifJsonWriter : ILsifJsonWriter
    {
        /// <summary>
        /// A lock that must be held when adding elements to <see cref="_elements"/>.
        /// </summary>
        private readonly object _elementsGate = new object();
        private List<Element> _elements = new List<Element>();
 
        /// <summary>
        /// A lock held when writing to ensure that we maintain ordering of elements across multiple threads.
        /// </summary>
        /// <remarks>
        /// The LSIF file format requires that any vertices referenced by an edge must be written before
        /// the edges that use that vertex. This creates some complexity because we want to be able to batch
        /// writes to the JSON as much as possible to avoid a lot of locking overhead. There's some shared state
        /// that we must track for global symbols, like IDs of various reference lists, and for us to add
        /// reference to those we need to ensure that shared state is flushed before we can write per-document state.
        /// 
        /// This lock is acquired during the entirety of a call to <see cref="FlushToUnderlyingAndEmpty"/>. That
        /// method wants to hold <see cref="_elementsGate"/> for a short as possible, to allow other threads
        /// producing new (unrelated) work to not be blocked behind us writing to the output. But if multiple threads
        /// are trying to flush the same thing, only one thread is going to acquire the list of items to flush.
        /// However, all threads need to wait until that flushing is complete before they can continue to write
        /// to the shared output. By having two locks, this lets us release <see cref="_elementsGate"/> as soon
        /// as we have the list of items, but we can still ensure that all callers to <see cref="FlushToUnderlyingAndEmpty"/>
        /// have waited for what they needed done.
        /// </remarks>
        private readonly object _writingGate = new object();
 
        private readonly ILsifJsonWriter _underlyingWriter;
 
        public BatchingLsifJsonWriter(ILsifJsonWriter underlyingWriter)
        {
            _underlyingWriter = underlyingWriter;
        }
 
        public void Write(Element element)
        {
            lock (_elementsGate)
            {
                _elements.Add(element);
            }
        }
 
        public void WriteAll(List<Element> elements)
        {
            lock (_elementsGate)
            {
                _elements.AddRange(elements);
            }
        }
 
        public void FlushToUnderlyingAndEmpty()
        {
            lock (_writingGate)
            {
                List<Element> localElements;
 
                lock (_elementsGate)
                {
                    localElements = _elements;
                    _elements = new List<Element>();
                }
 
                _underlyingWriter.WriteAll(localElements);
            }
        }
    }
}