File: Services\PerformanceTrackerServiceTests.cs
Web Access
Project: src\src\VisualStudio\Core\Test.Next\Roslyn.VisualStudio.Next.UnitTests.csproj (Roslyn.VisualStudio.Next.UnitTests)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Remote.Diagnostics;
using Xunit;
 
namespace Roslyn.VisualStudio.Next.UnitTests.Services;
 
public sealed class PerformanceTrackerServiceTests
{
    private const int TestMinSampleSizeForDocumentAnalysis = 100;
    private const int TestMinSampleSizeForSpanAnalysis = 10;
 
    [Theory, CombinatorialData]
    public void TestSampleSize(bool forSpanAnalysis)
    {
        var minSampleSize = forSpanAnalysis
            ? TestMinSampleSizeForSpanAnalysis
            : TestMinSampleSizeForDocumentAnalysis;
 
        // Verify no analyzer infos reported when sampleSize < minSampleSize
        var sampleSize = minSampleSize - 1;
        var analyzerInfos = GetAnalyzerInfos(sampleSize, forSpanAnalysis);
        Assert.Empty(analyzerInfos);
 
        // Verify analyzer infos reported when sampleSize >= minSampleSize
        sampleSize = minSampleSize + 1;
        analyzerInfos = GetAnalyzerInfos(sampleSize, forSpanAnalysis);
        Assert.NotEmpty(analyzerInfos);
    }
 
    [Theory, CombinatorialData]
    public void TestNoDuplicateReportGeneration(bool forSpanAnalysis)
    {
        var minSampleSize = forSpanAnalysis
            ? TestMinSampleSizeForSpanAnalysis
            : TestMinSampleSizeForDocumentAnalysis;
 
        var service = new PerformanceTrackerService(TestMinSampleSizeForDocumentAnalysis, TestMinSampleSizeForSpanAnalysis);
 
        // Verify analyzer infos reported when sampleSize >= minSampleSize
        var sampleSize = minSampleSize + 1;
        ReadTestFileAndAddSnapshots(service, sampleSize, forSpanAnalysis);
        var analyzerInfos = GenerateReport(service, forSpanAnalysis);
        Assert.NotEmpty(analyzerInfos);
 
        // Verify no analyzer infos reported when attempting to generate a duplicate
        // report without adding any new samples.
        analyzerInfos = GenerateReport(service, forSpanAnalysis);
        Assert.Empty(analyzerInfos);
 
        // Verify no analyzer infos reported after adding less than minSampleSize snapshots
        sampleSize = minSampleSize - 1;
        ReadTestFileAndAddSnapshots(service, sampleSize, forSpanAnalysis);
        analyzerInfos = GenerateReport(service, forSpanAnalysis);
        Assert.Empty(analyzerInfos);
 
        // Verify analyzer infos reported once we the added snapshots exceeds sample size
        sampleSize = 2;
        ReadTestFileAndAddSnapshots(service, sampleSize, forSpanAnalysis);
        analyzerInfos = GenerateReport(service, forSpanAnalysis);
        Assert.NotEmpty(analyzerInfos);
    }
 
    [Theory, CombinatorialData]
    public void TestTracking(bool forSpanAnalysis)
    {
        var analyzerInfos = GetAnalyzerInfos(to: 200, forSpanAnalysis);
 
        VerifyAnalyzerInfo(analyzerInfos, "CSharpRemoveUnnecessaryCastDiagnosticAnalyzer",
            mean: forSpanAnalysis ? 89.2416 : 54.48,
            stddev: forSpanAnalysis ? 78.8177 : 21.8163001442628);
        VerifyAnalyzerInfo(analyzerInfos, "CSharpInlineDeclarationDiagnosticAnalyzer",
            mean: forSpanAnalysis ? 48.4284 : 26.6686092715232,
            stddev: forSpanAnalysis ? 38.5010107523771 : 9.2987133054884);
        VerifyAnalyzerInfo(analyzerInfos, "VisualBasicRemoveUnnecessaryCastDiagnosticAnalyzer",
            mean: forSpanAnalysis ? 26.8618181818182 : 23.277619047619,
            stddev: forSpanAnalysis ? 6.62917030049974 : 7.25464266261805);
    }
 
    [Theory, CombinatorialData]
    public void TestTrackingMaxSample(bool forSpanAnalysis)
    {
        var analyzerInfos = GetAnalyzerInfos(to: 300, forSpanAnalysis);
 
        VerifyAnalyzerInfo(analyzerInfos, "CSharpRemoveUnnecessaryCastDiagnosticAnalyzer",
            mean: forSpanAnalysis ? 75.3678260869565 : 58.4542358078603,
            stddev: forSpanAnalysis ? 64.7106979026339 : 18.4245217226717);
        VerifyAnalyzerInfo(analyzerInfos, "VisualBasic.UseAutoProperty.UseAutoPropertyAnalyzer",
            mean: forSpanAnalysis ? 0.375 : 29.0622535211268,
            stddev: forSpanAnalysis ? 0.144592142784807 : 9.13728667060397);
        VerifyAnalyzerInfo(analyzerInfos, "CSharpInlineDeclarationDiagnosticAnalyzer",
            mean: forSpanAnalysis ? 37.6895652173913 : 28.7935371179039,
            stddev: forSpanAnalysis ? 29.5179969292277 : 7.99261581900397);
    }
 
    [Theory, CombinatorialData]
    public void TestTrackingRolling(bool forSpanAnalysis)
    {
        // data starting to rolling at 300 data points
        var analyzerInfos = GetAnalyzerInfos(to: 400, forSpanAnalysis);
 
        VerifyAnalyzerInfo(analyzerInfos, "CSharpRemoveUnnecessaryCastDiagnosticAnalyzer",
            mean: forSpanAnalysis ? 0.24304347826087 : 51.1698695652174,
            stddev: forSpanAnalysis ? 0.1511123654363 : 17.3819563479479);
        VerifyAnalyzerInfo(analyzerInfos, "VisualBasic.UseAutoProperty.UseAutoPropertyAnalyzer",
            mean: forSpanAnalysis ? 44.5428571428571 : 29.2597857142857,
            stddev: forSpanAnalysis ? 34.6949934844539 : 9.21213873850298);
        VerifyAnalyzerInfo(analyzerInfos, "InlineDeclaration.CSharpInlineDeclarationDiagnosticAnalyzer",
            mean: forSpanAnalysis ? 0.842608695652174 : 23.9764782608696,
            stddev: forSpanAnalysis ? 0.312682894617701 : 7.43956680199015);
    }
 
    [Fact]
    public void TestBadAnalyzerInfoPII()
    {
        var analyzerInfo1 = new AnalyzerInfoForPerformanceReporting(true, "test", 0.1, 0.1);
        Assert.True(analyzerInfo1.PIISafeAnalyzerId == analyzerInfo1.AnalyzerId);
        Assert.True(analyzerInfo1.PIISafeAnalyzerId == "test");
 
        var analyzerInfo2 = new AnalyzerInfoForPerformanceReporting(false, "test", 0.1, 0.1);
        Assert.True(analyzerInfo2.PIISafeAnalyzerId == analyzerInfo2.AnalyzerIdHash);
        Assert.True(analyzerInfo2.PIISafeAnalyzerId == "test".GetHashCode().ToString());
    }
 
    private static void VerifyAnalyzerInfo(List<AnalyzerInfoForPerformanceReporting> analyzerInfos, string analyzerName, double mean, double stddev)
    {
        var analyzerInfo = analyzerInfos.Single(i => i.AnalyzerId.Contains(analyzerName));
        Assert.True(analyzerInfo.PIISafeAnalyzerId.IndexOf(analyzerName, StringComparison.OrdinalIgnoreCase) >= 0);
        Assert.Equal(mean, analyzerInfo.Average, precision: 4);
        Assert.Equal(stddev, analyzerInfo.AdjustedStandardDeviation, precision: 4);
    }
 
    private static List<AnalyzerInfoForPerformanceReporting> GetAnalyzerInfos(int to, bool forSpanAnalysis)
    {
        var service = new PerformanceTrackerService(TestMinSampleSizeForDocumentAnalysis, TestMinSampleSizeForSpanAnalysis);
        ReadTestFileAndAddSnapshots(service, to, forSpanAnalysis);
        return GenerateReport(service, forSpanAnalysis);
    }
 
    private static void ReadTestFileAndAddSnapshots(PerformanceTrackerService service, int to, bool forSpanAnalysis)
    {
        var testFile = ReadTestFile(@"TestFiles\analyzer_input.csv");
 
        var (matrix, dataCount) = CreateMatrix(testFile);
 
        to = Math.Min(to, dataCount);
 
        for (var i = 0; i < to; i++)
        {
            service.AddSnapshot(CreateSnapshots(matrix, i), unitCount: 100, forSpanAnalysis);
        }
    }
 
    private static List<AnalyzerInfoForPerformanceReporting> GenerateReport(PerformanceTrackerService service, bool forSpanAnalysis)
    {
        var analyzerInfos = new List<AnalyzerInfoForPerformanceReporting>();
        service.GenerateReport(analyzerInfos, forSpanAnalysis);
 
        return analyzerInfos;
    }
 
    private static IEnumerable<AnalyzerPerformanceInfo> CreateSnapshots(Dictionary<string, double[]> matrix, int index)
    {
        foreach (var kv in matrix)
        {
            var timeSpan = kv.Value[index];
            if (double.IsNaN(timeSpan))
            {
                continue;
            }
 
            yield return new AnalyzerPerformanceInfo(kv.Key, true, TimeSpan.FromMilliseconds(timeSpan));
        }
    }
 
    private static (Dictionary<string, double[]> matrix, int dataCount) CreateMatrix(string testFile)
    {
        var matrix = new Dictionary<string, double[]>();
 
        var lines = testFile.Split('\n');
        var expectedDataCount = GetExpectedDataCount(lines[0]);
 
        for (var i = 1; i < lines.Length; i++)
        {
            if (lines[i].Trim().Length == 0)
            {
                continue;
            }
 
            var data = SkipAnalyzerId(lines[i]).Split(',');
            Assert.Equal(data.Length, expectedDataCount);
 
            var analyzerId = GetAnalyzerId(lines[i]);
 
            var timeSpans = new double[expectedDataCount];
            for (var j = 0; j < data.Length; j++)
            {
                double result;
                if (!double.TryParse(data[j], NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result))
                {
                    // no data for this analyzer for this particular run
                    result = double.NaN;
                }
 
                timeSpans[j] = result;
            }
 
            matrix[analyzerId] = timeSpans;
        }
 
        return (matrix, expectedDataCount);
    }
 
    private static string GetAnalyzerId(string line)
    {
        return line[1..line.LastIndexOf('"')];
    }
 
    private static int GetExpectedDataCount(string header)
    {
        var data = header.Split(',');
        return data.Length - 1;
    }
 
    private static string SkipAnalyzerId(string line)
    {
        return line[(line.LastIndexOf('"') + 2)..];
    }
 
    private static string ReadTestFile(string name)
    {
        var assembly = typeof(PerformanceTrackerServiceTests).Assembly;
        var resourceName = GetResourceName(assembly, name);
 
        using var stream = assembly.GetManifestResourceStream(resourceName);
        if (stream == null)
        {
            throw new InvalidOperationException($"Resource '{resourceName}' not found in {assembly.FullName}.");
        }
 
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
 
    private static string GetResourceName(Assembly assembly, string name)
    {
        var convert = name.Replace(@"\", ".");
 
        return assembly.GetManifestResourceNames().Where(n => n.EndsWith(convert, StringComparison.OrdinalIgnoreCase)).First();
    }
}