Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
70438a8
Increase the line cache count
CyrusNajmabadi May 23, 2024
5b9f6f1
Merge remote-tracking branch 'upstream/main' into lineCache
CyrusNajmabadi May 23, 2024
6594ef2
Less garbage
CyrusNajmabadi May 23, 2024
4fcdb6d
Docs
CyrusNajmabadi May 23, 2024
6cc4817
Rename file
CyrusNajmabadi May 23, 2024
0377096
Improve comment
CyrusNajmabadi May 23, 2024
c7420b3
Simplify
CyrusNajmabadi May 23, 2024
9675825
Use record
CyrusNajmabadi May 23, 2024
bf9ceb4
Docs
CyrusNajmabadi May 23, 2024
93558a0
Update src/EditorFeatures/Core/Classification/Syntactic/SyntacticClas…
CyrusNajmabadi May 23, 2024
e48e90b
Simplify
CyrusNajmabadi May 23, 2024
67ad43c
Merge branch 'lineCache' of https://github.com/CyrusNajmabadi/roslyn …
CyrusNajmabadi May 23, 2024
8d877a3
Don't reuse cache when doc-id or parse options change
CyrusNajmabadi May 23, 2024
d419f81
Pull out common logic
CyrusNajmabadi May 23, 2024
f615f51
Update src/Features/LanguageServer/Protocol/Handler/Diagnostics/Abstr…
CyrusNajmabadi May 23, 2024
6a2cc9e
Pass data through
CyrusNajmabadi May 23, 2024
ead5399
Simplify
CyrusNajmabadi May 23, 2024
244d514
Merge branch 'lineCache' of https://github.com/CyrusNajmabadi/roslyn …
CyrusNajmabadi May 23, 2024
c0972e9
Explicitness
CyrusNajmabadi May 23, 2024
137c4c0
Docs
CyrusNajmabadi May 23, 2024
4004fab
Pooling
CyrusNajmabadi May 23, 2024
6a81f8d
Formatting
CyrusNajmabadi May 23, 2024
90b48bf
Simplify
CyrusNajmabadi May 23, 2024
6180d36
Simplify
CyrusNajmabadi May 23, 2024
ab4acb9
remove contract
CyrusNajmabadi May 23, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// 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 System.Diagnostics;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Classification;

internal partial class SyntacticClassificationTaggerProvider
{
/// <summary>
/// Cache for storing recent classification results. This type has thread affinity on the UI thread, so it doesn't
/// need any additional synchronization.
/// </summary>
/// <remarks>
/// Empirical testing shows that when paging through a file, 25% of syntactic classification requests can be
/// returned from a simple LRU cache. When arrowing up and down in a file, the cache hit rate is 85%. This can
/// substantially speed up these requests, especially in large files, as it means we can avoid walking syntax
/// trees and reclassifying lines we just classified.
/// </remarks>
private sealed class ClassifiedLineCache(IThreadingContext threadingContext)
{
private readonly record struct SpanAndClassifiedSpans(Span Span, SegmentedList<ClassifiedSpan> ClassifiedSpans);

/// <summary>
/// Ensure that we don't cache incredibly long lines (for example, a minified JavaScript file). This also
/// ensures if we were asked by some party to classify a large section of the file, that we don't attempt to
/// store that. The primary purpose of this cache is for the common case of returning cached lines when the
/// editor calls into us to classify them.
/// </summary>
private const int MaxClassificationsCount = 1024;

/// <summary>
/// This number was picked as a reasonable number of lines to classify given regular screen sized. This is
/// about 6x larger than a normal number of lines in VS at standard resolutions, and handles the case of
/// users with vertical monitors and small font/zoom settings..
/// </summary>
private const int CacheSize = 256;

private static readonly ObjectPool<SegmentedList<ClassifiedSpan>> s_classifiedSpanListPool = new(() => new(), CacheSize);

private readonly IThreadingContext _threadingContext = threadingContext;

// Mutating state. No need for locks as we only execute on the UI thread (and throw if we're not on that thread).

/// <summary>
/// The last document id we cached. This can change when the user switches to another TFM (though the
/// ITextSnapshot will stay the same).
/// </summary>
private DocumentId? _documentId;

/// <summary>
/// The last parse options we computed classifications for. This can change when the user switches to another
/// mode (like Debug/Release). We want to dump classifications in that case even though the text snapshot is
/// the same.
/// </summary>
private ParseOptions? _parseOptions;

/// <summary>
/// The last text snapshot we cached classifications for. This can change when the user edits the file. When
/// that happens, we want to dump all previous classifications as they are no longer valid.
/// </summary>
private ITextSnapshot? _snapshot;

/// <summary>
/// LRU list of spans and the classifications for them. More recent entries are placed at the end of the list.
/// When we reach <see cref="CacheSize"/> we will start removing from the front. Items in the middle that are
/// used again are placed at the end of the list.
/// </summary>
private readonly LinkedList<SpanAndClassifiedSpans> _lruList = [];

/// <summary>
/// Mapping from span to the corresponding linked list node in <see cref="_lruList"/>. Allows us to quickly
/// lookup the cached classifications for a given span, as well as get directly to the linked list node, so we
/// can manipulate it easily. For example, removing it from its current location (in O(1) time) and adding it to
/// the end (also O(1)).
/// </summary>
private readonly Dictionary<Span, LinkedListNode<SpanAndClassifiedSpans>> _spanToLruNode = [];

private void ClearOnMajorChange(
DocumentId documentId,
ParseOptions? parseOptions,
ITextSnapshot snapshot)
{
_threadingContext.ThrowIfNotOnUIThread();

if (_documentId != documentId ||
_parseOptions != parseOptions ||
_snapshot != snapshot)
{
// Return all the classified span lists we allocated back to the pool.
foreach (var spanAndClassifiedSpans in _lruList)
s_classifiedSpanListPool.ClearAndFree(spanAndClassifiedSpans.ClassifiedSpans, trim: false);

_lruList.Clear();
_spanToLruNode.Clear();

_documentId = documentId;
_parseOptions = parseOptions;
_snapshot = snapshot;
}
}

public bool TryUseCache(
DocumentId documentId,
ParseOptions? parseOptions,
SnapshotSpan snapshotSpan,
SegmentedList<ClassifiedSpan> classifications)
{
_threadingContext.ThrowIfNotOnUIThread();

ClearOnMajorChange(documentId, parseOptions, snapshotSpan.Snapshot);

if (!_spanToLruNode.TryGetValue(snapshotSpan.Span, out var node))
return false;

// Move this node to the end of the LRU list.
_lruList.Remove(node);
_lruList.AddLast(node);

// AddRange is optimized to take a SegmentedList and copy directly from it into the result list.
classifications.AddRange(node.Value.ClassifiedSpans);
return true;
}

public void Update(SnapshotSpan snapshotSpan, SegmentedList<ClassifiedSpan> newClassifications)
{
_threadingContext.ThrowIfNotOnUIThread();

if (newClassifications.Count > MaxClassificationsCount)
return;

var span = snapshotSpan.Span;

if (_spanToLruNode.ContainsKey(span))
{
Debug.Fail("This should not be possible. Caller would have seen this item when calling TryUseCache");
return;
}

var node = GetOrCreateLruNode(span);

node.Value = node.Value with { Span = span };

// AddRange is optimized to take a SegmentedList and copy directly from it into the result list.
node.Value.ClassifiedSpans.Clear();
node.Value.ClassifiedSpans.AddRange(newClassifications);

_lruList.AddLast(node);
_spanToLruNode.Add(span, node);

Contract.ThrowIfTrue(_lruList.Count > CacheSize);
Contract.ThrowIfTrue(_lruList.Count != _spanToLruNode.Count);
}

private LinkedListNode<SpanAndClassifiedSpans> GetOrCreateLruNode(Span span)
{
if (_lruList.Count < CacheSize)
{
// We're not at capacity. Create a new node to go at the end of the LRU list.
return new LinkedListNode<SpanAndClassifiedSpans>(new SpanAndClassifiedSpans(span, s_classifiedSpanListPool.Allocate()));
}
else
{
// we're at capacity. Remove the oldest entry from the linked list. We'll use that as the node to add
// to the end of the LRU (replacing its contents with the new span/classified spans in the caller).

var firstNode = _lruList.First;
Contract.ThrowIfNull(firstNode);
_lruList.RemoveFirst();

// Now, remove the entry from the map as well.
Debug.Assert(_spanToLruNode[firstNode.Value.Span] == firstNode);
_spanToLruNode.Remove(firstNode.Value.Span);

return firstNode;
}
}
}
}

This file was deleted.

Loading