diff --git a/.editorconfig b/.editorconfig index 24c050dcf..52d718b36 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,6 +19,7 @@ csharp_new_line_before_open_brace = none csharp_style_var_for_built_in_types = true:warning csharp_style_var_when_type_is_apparent = true:warning csharp_style_var_elsewhere = true:warning +csharp_style_namespace_declarations=file_scoped:warning dotnet_style_readonly_field = true:warning @@ -29,4 +30,17 @@ dotnet_diagnostic.CA1822.severity = none dotnet_diagnostic.IDE0005.severity = warning # IDE0090: 'new' expression can be simplified. -dotnet_diagnostic.IDE0090.severity = warning \ No newline at end of file +dotnet_diagnostic.IDE0090.severity = warning + +# IDE0300: Use collection expression for array +# IDE0301: Use collection expression for empty +# IDE0302: Use collection expression for stackalloc +# IDE0303: Use collection expression for Create() +# IDE0304: Use collection expression for builder +# IDE0305: Use collection expression for fluent +dotnet_diagnostic.IDE0300.severity = warning +dotnet_diagnostic.IDE0301.severity = warning +dotnet_diagnostic.IDE0302.severity = warning +dotnet_diagnostic.IDE0303.severity = warning +dotnet_diagnostic.IDE0304.severity = warning +dotnet_diagnostic.IDE0305.severity = warning \ No newline at end of file diff --git a/source/Server/Caching/CachingMetrics.cs b/source/Server/Caching/CachingMetrics.cs deleted file mode 100644 index 7089af2fa..000000000 --- a/source/Server/Caching/CachingMetrics.cs +++ /dev/null @@ -1,10 +0,0 @@ -using SharpLab.Server.Monitoring; - -namespace SharpLab.Server.Caching { - public static class CachingMetrics { - public static MonitorMetric CacheableRequestCount { get; } = new("caching", "Caching: Cacheable Requests"); - public static MonitorMetric NoCacheRequestCount { get; } = new("caching", "Caching: No-Cache Requests"); - public static MonitorMetric BlobUploadRequestCount { get; } = new("caching", "Caching: Blob Upload Requests"); - public static MonitorMetric BlobUploadErrorCount { get; } = new("caching", "Caching: Blob Upload Errors"); - } -} diff --git a/source/Server/Caching/CachingModule.cs b/source/Server/Caching/CachingModule.cs index 494a36ae9..6cc682e94 100644 --- a/source/Server/Caching/CachingModule.cs +++ b/source/Server/Caching/CachingModule.cs @@ -3,21 +3,25 @@ using SharpLab.Server.Caching.Internal; using SharpLab.Server.Common; -namespace SharpLab.Server.Caching { - [UsedImplicitly] - public class CachingModule : Module { - protected override void Load(ContainerBuilder builder) { - var webAppName = EnvironmentHelper.GetRequiredEnvironmentVariable("SHARPLAB_WEBAPP_NAME"); - var branchId = webAppName.StartsWith("sl-") ? webAppName : null; +namespace SharpLab.Server.Caching; - builder.RegisterType() - .As() - .WithParameter("branchId", branchId) - .SingleInstance(); +[UsedImplicitly] +public class CachingModule : Module { + protected override void Load(ContainerBuilder builder) { + var webAppName = EnvironmentHelper.GetRequiredEnvironmentVariable("SHARPLAB_WEBAPP_NAME"); + var branchId = webAppName.StartsWith("sl-") ? webAppName : null; - builder.RegisterType() - .As() - .SingleInstance(); - } + builder.RegisterType() + .As() + .WithParameter("branchId", branchId) + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); } } \ No newline at end of file diff --git a/source/Server/Caching/CachingTracker.cs b/source/Server/Caching/CachingTracker.cs new file mode 100644 index 000000000..c740d1a02 --- /dev/null +++ b/source/Server/Caching/CachingTracker.cs @@ -0,0 +1,22 @@ +using SharpLab.Server.Monitoring; + +namespace SharpLab.Server.Caching; + +public class CachingTracker : ICachingTracker { + private readonly IMetricMonitor _cacheableRequestCountMonitor; + private readonly IMetricMonitor _noCacheRequestCountMonitor; + private readonly IMetricMonitor _blobUploadRequestCountMonitor; + private readonly IMetricMonitor _blobUploadErrorCountMonitor; + + public CachingTracker(MetricMonitorFactory createMonitor) { + _cacheableRequestCountMonitor = createMonitor("caching", "Caching: Cacheable Requests"); + _noCacheRequestCountMonitor = createMonitor("caching", "Caching: No-Cache Requests"); + _blobUploadRequestCountMonitor = createMonitor("caching", "Caching: Blob Upload Requests"); + _blobUploadErrorCountMonitor = createMonitor("caching", "Caching: Blob Upload Errors"); + } + + public void TrackCacheableRequest() => _cacheableRequestCountMonitor.Track(1); + public void TrackNoCacheRequest() => _noCacheRequestCountMonitor.Track(1); + public void TrackBlobUploadRequest() => _blobUploadRequestCountMonitor.Track(1); + public void TrackBlobUploadError() => _blobUploadErrorCountMonitor.Track(1); +} diff --git a/source/Server/Caching/ICachingTracker.cs b/source/Server/Caching/ICachingTracker.cs new file mode 100644 index 000000000..66463d35b --- /dev/null +++ b/source/Server/Caching/ICachingTracker.cs @@ -0,0 +1,8 @@ +namespace SharpLab.Server.Caching; + +public interface ICachingTracker { + void TrackBlobUploadError(); + void TrackBlobUploadRequest(); + void TrackCacheableRequest(); + void TrackNoCacheRequest(); +} \ No newline at end of file diff --git a/source/Server/Common/CommonModule.cs b/source/Server/Common/CommonModule.cs index 4d405a6db..22ddcb53d 100644 --- a/source/Server/Common/CommonModule.cs +++ b/source/Server/Common/CommonModule.cs @@ -8,79 +8,84 @@ using SharpLab.Server.Common.Languages; using SharpLab.Server.Compilation; -namespace SharpLab.Server.Common { - [UsedImplicitly] - public class CommonModule : Module { - protected override void Load(ContainerBuilder builder) { - RegisterExternals(builder); - - builder.RegisterInstance(MemoryPoolSlim.Shared); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - RegisterConfiguration(builder); - } - - private void RegisterConfiguration(ContainerBuilder builder) { - builder.RegisterType() - .As() - .SingleInstance() - .PreserveExistingDefaults(); - - builder.RegisterType() - .As() - .SingleInstance() - .PreserveExistingDefaults(); - } - - private void RegisterExternals(ContainerBuilder builder) { - builder.RegisterInstance(new RecyclableMemoryStreamManager()) - .AsSelf(); - - // older approach, needs to be updated to use factory now - builder.RegisterInstance>(() => new HttpClient()) - .As>() - .SingleInstance() - .PreserveExistingDefaults(); // allows tests and other overrides - - builder.RegisterType() - .As() - .SingleInstance(); - } +namespace SharpLab.Server.Common; +[UsedImplicitly] +public class CommonModule : Module { + protected override void Load(ContainerBuilder builder) { + RegisterExternals(builder); + + builder.RegisterInstance(MemoryPoolSlim.Shared); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + RegisterConfiguration(builder); + + var webAppName = EnvironmentHelper.GetRequiredEnvironmentVariable("SHARPLAB_WEBAPP_NAME"); + builder.RegisterType() + .As() + .SingleInstance() + .WithParameter("webAppName", webAppName); + } + + private void RegisterConfiguration(ContainerBuilder builder) { + builder.RegisterType() + .As() + .SingleInstance() + .PreserveExistingDefaults(); + + builder.RegisterType() + .As() + .SingleInstance() + .PreserveExistingDefaults(); + } + + private void RegisterExternals(ContainerBuilder builder) { + builder.RegisterInstance(new RecyclableMemoryStreamManager()) + .AsSelf(); + + // older approach, needs to be updated to use factory now + builder.RegisterInstance>(() => new HttpClient()) + .As>() + .SingleInstance() + .PreserveExistingDefaults(); // allows tests and other overrides + + builder.RegisterType() + .As() + .SingleInstance(); } } diff --git a/source/Server/Common/FeatureTracker.cs b/source/Server/Common/FeatureTracker.cs new file mode 100644 index 000000000..ec32478a2 --- /dev/null +++ b/source/Server/Common/FeatureTracker.cs @@ -0,0 +1,51 @@ +using SharpLab.Server.Monitoring; +using System.Collections.Generic; +using System.Linq; + +namespace SharpLab.Server.Common; + +public class FeatureTracker : IFeatureTracker { + private readonly IMetricMonitor _branchMonitor; + private readonly IReadOnlyDictionary _languageMetricMonitors; + private readonly IReadOnlyDictionary _targetMetricMonitors; + private readonly IMetricMonitor _optimizeDebugMonitor; + private readonly IMetricMonitor _optimizeReleaseMonitor; + + public FeatureTracker(MetricMonitorFactory createMonitor, string webAppName) { + _branchMonitor = createMonitor("feature", $"Branch: {webAppName}"); + _languageMetricMonitors = LanguageNames.All.ToDictionary( + name => name, + name => createMonitor("feature", $"Language: {name}") + ); + _targetMetricMonitors = TargetNames.All.ToDictionary( + name => name, + name => createMonitor("feature", $"Target: {name}") + ); + + _optimizeDebugMonitor = createMonitor("feature", "Optimize: Debug"); + _optimizeReleaseMonitor = createMonitor("feature", "Optimize: Release"); + } + + public void TrackBranch() { + _branchMonitor.Track(1); + } + + public void TrackLanguage(string languageName) { + if (_languageMetricMonitors.TryGetValue(languageName, out var metricMonitor)) + metricMonitor.Track(1); + } + + public void TrackTarget(string targetName) { + if (_targetMetricMonitors.TryGetValue(targetName, out var metricMonitor)) + metricMonitor.Track(1); + } + + public void TrackOptimize(string? optimize) { + var monitor = optimize switch { + Optimize.Debug => _optimizeDebugMonitor, + Optimize.Release => _optimizeReleaseMonitor, + _ => null + }; + monitor?.Track(1); + } +} diff --git a/source/Server/Common/IFeatureTracker.cs b/source/Server/Common/IFeatureTracker.cs new file mode 100644 index 000000000..c1e4053fb --- /dev/null +++ b/source/Server/Common/IFeatureTracker.cs @@ -0,0 +1,8 @@ +namespace SharpLab.Server.Common; + +public interface IFeatureTracker { + void TrackBranch(); + void TrackLanguage(string languageName); + void TrackTarget(string targetName); + void TrackOptimize(string? optimize); +} \ No newline at end of file diff --git a/source/Server/Common/ILanguageAdapter.cs b/source/Server/Common/ILanguageAdapter.cs index 8fe919dfe..02b2db209 100644 --- a/source/Server/Common/ILanguageAdapter.cs +++ b/source/Server/Common/ILanguageAdapter.cs @@ -3,18 +3,17 @@ using MirrorSharp.Advanced; using SharpLab.Server.Common.Internal; -namespace SharpLab.Server.Common { - public interface ILanguageAdapter { - string LanguageName { get; } +namespace SharpLab.Server.Common; +public interface ILanguageAdapter { + string LanguageName { get; } - void SlowSetup(MirrorSharpOptions options); - void SetOptimize(IWorkSession session, string optimize); - void SetOptionsForTarget(IWorkSession session, string target); + void SlowSetup(MirrorSharpOptions options); + void SetOptimize(IWorkSession session, string optimize); + void SetOptionsForTarget(IWorkSession session, string target); - ImmutableArray GetMethodParameterLines(IWorkSession session, int lineInMethod, int columnInMethod); - ImmutableArray GetCallArgumentIdentifiers(IWorkSession session, int callStartLine, int callStartColumn); + ImmutableArray GetMethodParameterLines(IWorkSession session, int lineInMethod, int columnInMethod); + ImmutableArray GetCallArgumentIdentifiers(IWorkSession session, int callStartLine, int callStartColumn); - // Note: in some cases this Task is never resolved (e.g. if VB is never used) - AssemblyReferenceDiscoveryTask AssemblyReferenceDiscoveryTask { get; } - } + // Note: in some cases this Task is never resolved (e.g. if VB is never used) + AssemblyReferenceDiscoveryTask AssemblyReferenceDiscoveryTask { get; } } \ No newline at end of file diff --git a/source/Server/Common/LanguageNames.cs b/source/Server/Common/LanguageNames.cs index c2ec52785..73828ccda 100644 --- a/source/Server/Common/LanguageNames.cs +++ b/source/Server/Common/LanguageNames.cs @@ -1,10 +1,15 @@ +using System.Collections.Immutable; using CodeAnalysis = Microsoft.CodeAnalysis; -namespace SharpLab.Server.Common { - public class LanguageNames { - public const string CSharp = CodeAnalysis.LanguageNames.CSharp; - public const string VisualBasic = CodeAnalysis.LanguageNames.VisualBasic; - public const string FSharp = CodeAnalysis.LanguageNames.FSharp; - public const string IL = "IL"; - } +namespace SharpLab.Server.Common; +public class LanguageNames { + public const string CSharp = CodeAnalysis.LanguageNames.CSharp; + public const string VisualBasic = CodeAnalysis.LanguageNames.VisualBasic; + public const string FSharp = CodeAnalysis.LanguageNames.FSharp; + public const string IL = "IL"; + + + public static readonly ImmutableArray All = [ + CSharp, VisualBasic, FSharp, IL + ]; } diff --git a/source/Server/Common/Languages/CSharpAdapter.cs b/source/Server/Common/Languages/CSharpAdapter.cs index 078bcc7ec..4f748773a 100644 --- a/source/Server/Common/Languages/CSharpAdapter.cs +++ b/source/Server/Common/Languages/CSharpAdapter.cs @@ -13,145 +13,145 @@ using SharpLab.Server.Compilation; using SharpLab.Server.Compilation.Internal; -namespace SharpLab.Server.Common.Languages { - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public class CSharpAdapter : ILanguageAdapter { - private static readonly LanguageVersion MaxLanguageVersion = Enum - .GetValues(typeof (LanguageVersion)) - .Cast() - .Where(v => v != LanguageVersion.Latest) // seems like latest got fixed at some point - .Max(); - private static readonly ImmutableArray ReleasePreprocessorSymbols = PreprocessorSymbols.Release.Add("__DEMO_EXPERIMENTAL__"); - private static readonly ImmutableArray DebugPreprocessorSymbols = PreprocessorSymbols.Debug.Add("__DEMO_EXPERIMENTAL__"); - - private readonly ImmutableList _references; - private readonly ICSharpTopLevelProgramSupport _topLevelProgramSupport; - - public CSharpAdapter( - IAssemblyPathCollector assemblyPathCollector, - IAssemblyDocumentationResolver documentationResolver, - ICSharpTopLevelProgramSupport topLevelProgramSupport - ) { - var referencedAssemblyPaths = assemblyPathCollector.SlowGetAllAssemblyPathsIncludingReferences( - // Essential - NetFrameworkRuntime.AssemblyOfValueTask.GetName().Name!, - NetFrameworkRuntime.AssemblyOfValueTuple.GetName().Name!, - NetFrameworkRuntime.AssemblyOfSpan.GetName().Name!, - "Microsoft.CSharp", - - // Runtime - "SharpLab.Runtime", - - // Requested - "System.Collections.Immutable", - "System.Data", - "System.Runtime.CompilerServices.Unsafe", - "System.Runtime.Intrinsics", - "System.Text.Json", - "System.Web.HttpUtility", - "System.Xml.Linq" - ).ToImmutableList(); - - var assemblyReferenceTaskSource = new AssemblyReferenceDiscoveryTaskSource(); - assemblyReferenceTaskSource.Complete(referencedAssemblyPaths); - AssemblyReferenceDiscoveryTask = assemblyReferenceTaskSource.Task; - - _references = referencedAssemblyPaths - .Select(path => (MetadataReference)MetadataReference.CreateFromFile(path, documentation: documentationResolver.GetDocumentation(path))) - .ToImmutableList(); - _topLevelProgramSupport = topLevelProgramSupport; - } +namespace SharpLab.Server.Common.Languages; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public class CSharpAdapter : ILanguageAdapter { + private static readonly LanguageVersion MaxLanguageVersion = Enum + .GetValues(typeof (LanguageVersion)) + .Cast() + .Where(v => v != LanguageVersion.Latest) // seems like latest got fixed at some point + .Max(); + private static readonly ImmutableArray ReleasePreprocessorSymbols = PreprocessorSymbols.Release.Add("__DEMO_EXPERIMENTAL__"); + private static readonly ImmutableArray DebugPreprocessorSymbols = PreprocessorSymbols.Debug.Add("__DEMO_EXPERIMENTAL__"); + + private readonly ImmutableList _references; + private readonly ICSharpTopLevelProgramSupport _topLevelProgramSupport; + + public CSharpAdapter( + IAssemblyPathCollector assemblyPathCollector, + IAssemblyDocumentationResolver documentationResolver, + ICSharpTopLevelProgramSupport topLevelProgramSupport + ) { + var referencedAssemblyPaths = assemblyPathCollector.SlowGetAllAssemblyPathsIncludingReferences( + // Essential + NetFrameworkRuntime.AssemblyOfValueTask.GetName().Name!, + NetFrameworkRuntime.AssemblyOfValueTuple.GetName().Name!, + NetFrameworkRuntime.AssemblyOfSpan.GetName().Name!, + "Microsoft.CSharp", + + // Runtime + "SharpLab.Runtime", + + // Requested + "System.Collections.Immutable", + "System.Data", + "System.Runtime.CompilerServices.Unsafe", + "System.Runtime.Intrinsics", + "System.Text.Json", + "System.Web.HttpUtility", + "System.Xml.Linq" + ).ToImmutableList(); + + var assemblyReferenceTaskSource = new AssemblyReferenceDiscoveryTaskSource(); + assemblyReferenceTaskSource.Complete(referencedAssemblyPaths); + AssemblyReferenceDiscoveryTask = assemblyReferenceTaskSource.Task; + + _references = referencedAssemblyPaths + .Select(path => (MetadataReference)MetadataReference.CreateFromFile(path, documentation: documentationResolver.GetDocumentation(path))) + .ToImmutableList(); + _topLevelProgramSupport = topLevelProgramSupport; + } - public string LanguageName => LanguageNames.CSharp; - public AssemblyReferenceDiscoveryTask AssemblyReferenceDiscoveryTask { get; } - - public void SlowSetup(MirrorSharpOptions options) { - // ReSharper disable HeapView.ObjectAllocation.Evident - - options.CSharp.ParseOptions = new CSharpParseOptions( - MaxLanguageVersion, - preprocessorSymbols: DebugPreprocessorSymbols, - documentationMode: DocumentationMode.Diagnose - ); - options.CSharp.CompilationOptions = new CSharpCompilationOptions( - OutputKind.DynamicallyLinkedLibrary, - specificDiagnosticOptions: new Dictionary { - // CS1591: Missing XML comment for publicly visible type or member - { "CS1591", ReportDiagnostic.Suppress } - }, - allowUnsafe: true, - nullableContextOptions: NullableContextOptions.Enable - ); - options.CSharp.MetadataReferences = _references; - - // ReSharper restore HeapView.ObjectAllocation.Evident - } + public string LanguageName => LanguageNames.CSharp; + public AssemblyReferenceDiscoveryTask AssemblyReferenceDiscoveryTask { get; } + + public void SlowSetup(MirrorSharpOptions options) { + // ReSharper disable HeapView.ObjectAllocation.Evident + + options.CSharp.ParseOptions = new CSharpParseOptions( + MaxLanguageVersion, + preprocessorSymbols: DebugPreprocessorSymbols, + documentationMode: DocumentationMode.Diagnose + ); + options.CSharp.CompilationOptions = new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + specificDiagnosticOptions: new Dictionary { + // CS1591: Missing XML comment for publicly visible type or member + { "CS1591", ReportDiagnostic.Suppress } + }, + allowUnsafe: true, + nullableContextOptions: NullableContextOptions.Enable + ); + options.CSharp.MetadataReferences = _references; + + // ReSharper restore HeapView.ObjectAllocation.Evident + } - public void SetOptimize(IWorkSession session, string optimize) { - var project = session.Roslyn.Project; - var parseOptions = ((CSharpParseOptions)project.ParseOptions!); - var compilationOptions = ((CSharpCompilationOptions)project.CompilationOptions!); - session.Roslyn.Project = project - .WithParseOptions(parseOptions.WithPreprocessorSymbols(optimize == Optimize.Debug ? DebugPreprocessorSymbols : ReleasePreprocessorSymbols)) - .WithCompilationOptions(compilationOptions.WithOptimizationLevel(optimize == Optimize.Debug ? OptimizationLevel.Debug : OptimizationLevel.Release)); - } + public void SetOptimize(IWorkSession session, string optimize) { + var project = session.Roslyn.Project; + var parseOptions = ((CSharpParseOptions)project.ParseOptions!); + var compilationOptions = ((CSharpCompilationOptions)project.CompilationOptions!); + session.Roslyn.Project = project + .WithParseOptions(parseOptions.WithPreprocessorSymbols(optimize == Optimize.Debug ? DebugPreprocessorSymbols : ReleasePreprocessorSymbols)) + .WithCompilationOptions(compilationOptions.WithOptimizationLevel(optimize == Optimize.Debug ? OptimizationLevel.Debug : OptimizationLevel.Release)); + } - public void SetOptionsForTarget(IWorkSession session, string target) { - var outputKind = target is TargetNames.Run or TargetNames.RunIL - ? OutputKind.ConsoleApplication - : OutputKind.DynamicallyLinkedLibrary; + public void SetOptionsForTarget(IWorkSession session, string target) { + var outputKind = target is TargetNames.Run or TargetNames.RunIL + ? OutputKind.ConsoleApplication + : OutputKind.DynamicallyLinkedLibrary; - var project = session.Roslyn.Project; - var options = ((CSharpCompilationOptions)project.CompilationOptions!); - session.Roslyn.Project = project.WithCompilationOptions( - options.WithOutputKind(outputKind) - ); + var project = session.Roslyn.Project; + var options = ((CSharpCompilationOptions)project.CompilationOptions!); + session.Roslyn.Project = project.WithCompilationOptions( + options.WithOutputKind(outputKind) + ); - _topLevelProgramSupport.UpdateOutputKind(session); - } + _topLevelProgramSupport.UpdateOutputKind(session); + } - public ImmutableArray GetMethodParameterLines(IWorkSession session, int lineInMethod, int columnInMethod) { - var declaration = RoslynAdapterHelper.FindSyntaxNodeInSession(session, lineInMethod, columnInMethod) - ?.AncestorsAndSelf() - .FirstOrDefault(m => m is MemberDeclarationSyntax - || m is AnonymousFunctionExpressionSyntax - || m is LocalFunctionStatementSyntax); - - var parameters = declaration switch { - BaseMethodDeclarationSyntax m => m.ParameterList.Parameters, - ParenthesizedLambdaExpressionSyntax l => l.ParameterList.Parameters, - SimpleLambdaExpressionSyntax l => SyntaxFactory.SingletonSeparatedList(l.Parameter), - LocalFunctionStatementSyntax f => f.ParameterList.Parameters, - _ => SyntaxFactory.SeparatedList() - }; - - if (parameters.Count == 0) - return ImmutableArray.Empty; - - var results = new int[parameters.Count]; - for (var i = 0; i < parameters.Count; i++) { - results[i] = parameters[i].GetLocation().GetLineSpan().StartLinePosition.Line + 1; - } - return ImmutableArray.Create(results); + public ImmutableArray GetMethodParameterLines(IWorkSession session, int lineInMethod, int columnInMethod) { + var declaration = RoslynAdapterHelper.FindSyntaxNodeInSession(session, lineInMethod, columnInMethod) + ?.AncestorsAndSelf() + .FirstOrDefault(m => m is MemberDeclarationSyntax + || m is AnonymousFunctionExpressionSyntax + || m is LocalFunctionStatementSyntax); + + var parameters = declaration switch { + BaseMethodDeclarationSyntax m => m.ParameterList.Parameters, + ParenthesizedLambdaExpressionSyntax l => l.ParameterList.Parameters, + SimpleLambdaExpressionSyntax l => SyntaxFactory.SingletonSeparatedList(l.Parameter), + LocalFunctionStatementSyntax f => f.ParameterList.Parameters, + _ => SyntaxFactory.SeparatedList() + }; + + if (parameters.Count == 0) + return []; + + var results = new int[parameters.Count]; + for (var i = 0; i < parameters.Count; i++) { + results[i] = parameters[i].GetLocation().GetLineSpan().StartLinePosition.Line + 1; } + return ImmutableArray.Create(results); + } - public ImmutableArray GetCallArgumentIdentifiers([NotNull] IWorkSession session, int callStartLine, int callStartColumn) { - var call = RoslynAdapterHelper.FindSyntaxNodeInSession(session, callStartLine, callStartColumn) - ?.AncestorsAndSelf() - .OfType() - .FirstOrDefault(); - if (call == null) - return ImmutableArray.Empty; - - var arguments = call.ArgumentList.Arguments; - if (arguments.Count == 0) - return ImmutableArray.Empty; - - var results = new string?[arguments.Count]; - for (var i = 0; i < arguments.Count; i++) { - results[i] = (arguments[i].Expression is IdentifierNameSyntax n) ? n.Identifier.ValueText : null; - } - return ImmutableArray.Create(results); + public ImmutableArray GetCallArgumentIdentifiers([NotNull] IWorkSession session, int callStartLine, int callStartColumn) { + var call = RoslynAdapterHelper.FindSyntaxNodeInSession(session, callStartLine, callStartColumn) + ?.AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + if (call == null) + return []; + + var arguments = call.ArgumentList.Arguments; + if (arguments.Count == 0) + return []; + + var results = new string?[arguments.Count]; + for (var i = 0; i < arguments.Count; i++) { + results[i] = (arguments[i].Expression is IdentifierNameSyntax n) ? n.Identifier.ValueText : null; } + return ImmutableArray.Create(results); } } diff --git a/source/Server/Common/Optimize.cs b/source/Server/Common/Optimize.cs index daf0b91ec..d4b107e1b 100644 --- a/source/Server/Common/Optimize.cs +++ b/source/Server/Common/Optimize.cs @@ -1,6 +1,5 @@ -namespace SharpLab.Server.Common { - public static class Optimize { - public const string Debug = "debug"; - public const string Release = "release"; - } +namespace SharpLab.Server.Common; +public static class Optimize { + public const string Debug = "debug"; + public const string Release = "release"; } diff --git a/source/Server/Common/TargetNames.cs b/source/Server/Common/TargetNames.cs index ed6a05b50..c1f265ed9 100644 --- a/source/Server/Common/TargetNames.cs +++ b/source/Server/Common/TargetNames.cs @@ -1,12 +1,17 @@ -namespace SharpLab.Server.Common { - public static class TargetNames { - public const string CSharp = LanguageNames.CSharp; - public const string IL = LanguageNames.IL; - public const string Ast = "AST"; - public const string JitAsm = "JIT ASM"; - public const string Run = "Run"; - public const string RunIL = "Run IL"; - public const string Verify = "Verify"; - public const string Explain = "Explain"; - } +using System.Collections.Immutable; + +namespace SharpLab.Server.Common; +public static class TargetNames { + public const string CSharp = LanguageNames.CSharp; + public const string IL = LanguageNames.IL; + public const string Ast = "AST"; + public const string JitAsm = "JIT ASM"; + public const string Run = "Run"; + public const string RunIL = "Run IL"; + public const string Verify = "Verify"; + public const string Explain = "Explain"; + + public static readonly ImmutableArray All = [ + CSharp, IL, Ast, JitAsm, Run, RunIL, Verify, Explain + ]; } \ No newline at end of file diff --git a/source/Server/Compilation/ICompiler.cs b/source/Server/Compilation/ICompiler.cs index 87a206f54..3a1409151 100644 --- a/source/Server/Compilation/ICompiler.cs +++ b/source/Server/Compilation/ICompiler.cs @@ -5,14 +5,13 @@ using Microsoft.CodeAnalysis; using MirrorSharp.Advanced; -namespace SharpLab.Server.Compilation { - public interface ICompiler { - Task<(bool assembly, bool symbols)> TryCompileToStreamAsync( - MemoryStream assemblyStream, - MemoryStream? symbolStream, - IWorkSession session, - IList diagnostics, - CancellationToken cancellationToken - ); - } +namespace SharpLab.Server.Compilation; +public interface ICompiler { + Task<(bool assembly, bool symbols)> TryCompileToStreamAsync( + MemoryStream assemblyStream, + MemoryStream? symbolStream, + IWorkSession session, + IList diagnostics, + CancellationToken cancellationToken + ); } \ No newline at end of file diff --git a/source/Server/Execution/Container/ContainerExperimentMetrics.cs b/source/Server/Execution/Container/ContainerExperimentMetrics.cs deleted file mode 100644 index 0ce31b969..000000000 --- a/source/Server/Execution/Container/ContainerExperimentMetrics.cs +++ /dev/null @@ -1,8 +0,0 @@ -using SharpLab.Server.Monitoring; - -namespace SharpLab.Server.Execution.Container { - public static class ContainerExperimentMetrics { - public static MonitorMetric ContainerRunCount { get; } = new("container-experiment", "Runs: Container"); - public static MonitorMetric ContainerFailureCount { get; } = new("container-experiment", "Runs: Failed"); - } -} diff --git a/source/Server/Explanation/Internal/ExternalSyntaxExplanationProvider.cs b/source/Server/Explanation/Internal/ExternalSyntaxExplanationProvider.cs index 68b86b1df..4b61a7d09 100644 --- a/source/Server/Explanation/Internal/ExternalSyntaxExplanationProvider.cs +++ b/source/Server/Explanation/Internal/ExternalSyntaxExplanationProvider.cs @@ -3,131 +3,130 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using SharpLab.Server.Monitoring; using SharpYaml.Serialization; using SourcePath; using SourcePath.Roslyn; -using SharpLab.Server.Monitoring; - -namespace SharpLab.Server.Explanation.Internal { - public class ExternalSyntaxExplanationProvider : ISyntaxExplanationProvider, IDisposable { - private readonly Func _httpClientFactory; - private readonly ExternalSyntaxExplanationSettings _settings; - private IReadOnlyCollection? _explanations; - private readonly SemaphoreSlim _explanationsLock = new(1); +namespace SharpLab.Server.Explanation.Internal; +public class ExternalSyntaxExplanationProvider : ISyntaxExplanationProvider, IDisposable { + private readonly Func _httpClientFactory; + private readonly ExternalSyntaxExplanationSettings _settings; - private Task? _updateTask; - private CancellationTokenSource? _updateCancellationSource; + private IReadOnlyCollection? _explanations; + private readonly SemaphoreSlim _explanationsLock = new(1); - private readonly Serializer _serilializer = new (new() { - NamingConvention = new FlatNamingConvention() - }); - private readonly IMonitor _monitor; - private readonly ISourcePathParser _sourcePathParser; - - public ExternalSyntaxExplanationProvider( - Func httpClientFactory, - ExternalSyntaxExplanationSettings settings, - ISourcePathParser sourcePathParser, - IMonitor monitor - ) { - _httpClientFactory = httpClientFactory; - _settings = settings; - _sourcePathParser = sourcePathParser; - _monitor = monitor; - } + private Task? _updateTask; + private CancellationTokenSource? _updateCancellationSource; - public async ValueTask> GetExplanationsAsync(CancellationToken cancellationToken) { - if (_explanations == null) { - try { - await _explanationsLock.WaitAsync(cancellationToken).ConfigureAwait(false); - if (_explanations != null) - return _explanations; - _explanations = await LoadExplanationsSlowAsync(cancellationToken).ConfigureAwait(false); - _updateCancellationSource = new CancellationTokenSource(); - _updateTask = Task.Run(UpdateLoopAsync); - } - finally { - _explanationsLock.Release(); - } - } + private readonly Serializer _serilializer = new (new() { + NamingConvention = new FlatNamingConvention() + }); + private readonly IExceptionMonitor _exceptionMonitor; + private readonly ISourcePathParser _sourcePathParser; - return _explanations; - } - - private async Task> LoadExplanationsSlowAsync(CancellationToken cancellationToken) { - var explanations = new List(); - var serializer = new Serializer(); - using (var client = _httpClientFactory()) { - var response = await client.GetAsync(_settings.SourceUrl, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - - var yamlString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - var yaml = _serilializer.Deserialize>(yamlString); - foreach (var item in yaml) { - SyntaxExplanation parsed; - try { - parsed = ParseExplanation(item); - } - catch (Exception ex) { - // depending on SourcePath version, it's possible that - // an explanation fails to parse on some branches - _monitor.Exception(ex, session: null); - continue; - } - explanations.Add(parsed); - } - } - return explanations; - } + public ExternalSyntaxExplanationProvider( + Func httpClientFactory, + ExternalSyntaxExplanationSettings settings, + ISourcePathParser sourcePathParser, + IExceptionMonitor exceptionMonitor + ) { + _httpClientFactory = httpClientFactory; + _settings = settings; + _sourcePathParser = sourcePathParser; + _exceptionMonitor = exceptionMonitor; + } - private SyntaxExplanation ParseExplanation(YamlExplanation item) { - ISourcePath path; + public async ValueTask> GetExplanationsAsync(CancellationToken cancellationToken) { + if (_explanations == null) { try { - path = _sourcePathParser.Parse(item.Path); + await _explanationsLock.WaitAsync(cancellationToken).ConfigureAwait(false); + if (_explanations != null) + return _explanations; + _explanations = await LoadExplanationsSlowAsync(cancellationToken).ConfigureAwait(false); + _updateCancellationSource = new CancellationTokenSource(); + _updateTask = Task.Run(UpdateLoopAsync); } - catch (Exception ex) { - throw new Exception($"Failed to parse path for '{item.Name}': {ex.Message}.", ex); + finally { + _explanationsLock.Release(); } - return new SyntaxExplanation(path, item.Name!, item.Text!, item.Link!); } - private async Task UpdateLoopAsync() { - while (!_updateCancellationSource!.IsCancellationRequested) { - try { - await Task.Delay(_settings.UpdatePeriod, _updateCancellationSource.Token).ConfigureAwait(false); - } - catch (TaskCanceledException) { - return; - } + return _explanations; + } + + private async Task> LoadExplanationsSlowAsync(CancellationToken cancellationToken) { + var explanations = new List(); + var serializer = new Serializer(); + using (var client = _httpClientFactory()) { + var response = await client.GetAsync(_settings.SourceUrl, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + var yamlString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var yaml = _serilializer.Deserialize>(yamlString); + foreach (var item in yaml) { + SyntaxExplanation parsed; try { - _explanations = await LoadExplanationsSlowAsync(_updateCancellationSource.Token).ConfigureAwait(false); + parsed = ParseExplanation(item); } catch (Exception ex) { - _monitor.Exception(ex, session: null); - // intentionally not re-throwing -- retrying after delay + // depending on SourcePath version, it's possible that + // an explanation fails to parse on some branches + _exceptionMonitor.Exception(ex, session: null); + continue; } + explanations.Add(parsed); } } + return explanations; + } - public void Dispose() { - DisposeAsync().Wait(TimeSpan.FromMinutes(1)); + private SyntaxExplanation ParseExplanation(YamlExplanation item) { + ISourcePath path; + try { + path = _sourcePathParser.Parse(item.Path); } + catch (Exception ex) { + throw new Exception($"Failed to parse path for '{item.Name}': {ex.Message}.", ex); + } + return new SyntaxExplanation(path, item.Name!, item.Text!, item.Link!); + } - public async Task DisposeAsync() { - using (_updateCancellationSource) { - if (_updateTask == null) - return; - _updateCancellationSource!.Cancel(); - await _updateTask.ConfigureAwait(true); + private async Task UpdateLoopAsync() { + while (!_updateCancellationSource!.IsCancellationRequested) { + try { + await Task.Delay(_settings.UpdatePeriod, _updateCancellationSource.Token).ConfigureAwait(false); + } + catch (TaskCanceledException) { + return; + } + try { + _explanations = await LoadExplanationsSlowAsync(_updateCancellationSource.Token).ConfigureAwait(false); + } + catch (Exception ex) { + _exceptionMonitor.Exception(ex, session: null); + // intentionally not re-throwing -- retrying after delay } } + } - private class YamlExplanation { - public string? Name { get; set; } - public string? Text { get; set; } - public string? Link { get; set; } - public string? Path { get; set; } + public void Dispose() { + DisposeAsync().Wait(TimeSpan.FromMinutes(1)); + } + + public async Task DisposeAsync() { + using (_updateCancellationSource) { + if (_updateTask == null) + return; + _updateCancellationSource!.Cancel(); + await _updateTask.ConfigureAwait(true); } } + + private class YamlExplanation { + public string? Name { get; set; } + public string? Text { get; set; } + public string? Link { get; set; } + public string? Path { get; set; } + } } diff --git a/source/Server/Integration/Azure/ApplicationInsightsExceptionMonitor.cs b/source/Server/Integration/Azure/ApplicationInsightsExceptionMonitor.cs new file mode 100644 index 000000000..8b36fb9e4 --- /dev/null +++ b/source/Server/Integration/Azure/ApplicationInsightsExceptionMonitor.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; +using MirrorSharp.Advanced; +using MirrorSharp.Internal; +using Newtonsoft.Json; +using SharpLab.Server.MirrorSharp; +using SharpLab.Server.Monitoring; + +namespace SharpLab.Server.Integration.Azure; +public class ApplicationInsightsExceptionMonitor : IExceptionMonitor { + private readonly TelemetryClient _client; + private readonly string _webAppName; + + public ApplicationInsightsExceptionMonitor(TelemetryClient client, string webAppName) { + _client = Argument.NotNull(nameof(client), client); + _webAppName = Argument.NotNullOrEmpty(nameof(webAppName), webAppName); + } + + public void Exception(Exception exception, IWorkSession? session, IDictionary? extras = null) { + var sessionInternals = session as WorkSession; + var telemetry = new ExceptionTelemetry(exception) { + Context = { Session = { Id = session?.GetSessionId() } }, + Properties = { + { "Web App", _webAppName }, + { "Code", session?.GetText() }, + { "Language", session?.LanguageName }, + { "Target", session?.GetTargetName() }, + { "Cursor", sessionInternals?.CursorPosition.ToString() }, + { "Completion", FormatCompletion(sessionInternals) } + } + }; + if (extras != null) { + foreach (var pair in extras) { + telemetry.Properties.Add(pair.Key, pair.Value); + } + } + _client.TrackException(telemetry); + } + + private string? FormatCompletion(WorkSession? session) { + try { + if (session == null) + return null; + + var current = session.CurrentCompletion; + if (current.List == null && !current.ChangeEchoPending && current.PendingChar == null) + return null; + + return JsonConvert.ToString(new { + List = current.List is { } list ? new { + Items = new { + Take10 = list.ItemsList.Take(10), + list.ItemsList.Count + }, + list.Span + } : null, + current.ChangeEchoPending, + current.PendingChar + }); + } + catch (Exception ex) { + return ""; + } + } +} diff --git a/source/Server/Integration/Azure/ApplicationInsightsMetricMonitor.cs b/source/Server/Integration/Azure/ApplicationInsightsMetricMonitor.cs new file mode 100644 index 000000000..f72565e08 --- /dev/null +++ b/source/Server/Integration/Azure/ApplicationInsightsMetricMonitor.cs @@ -0,0 +1,17 @@ +using Microsoft.ApplicationInsights; +using SharpLab.Server.Monitoring; + +namespace SharpLab.Server.Integration.Azure; +internal class ApplicationInsightsMetricMonitor : IMetricMonitor { + private readonly Metric _metric; + + public ApplicationInsightsMetricMonitor(Metric metric) { + Argument.NotNull(nameof(metric), metric); + + _metric = metric; + } + + public void Track(double value) { + _metric.TrackValue(value); + } +} \ No newline at end of file diff --git a/source/Server/Integration/Azure/ApplicationInsightsMonitor.cs b/source/Server/Integration/Azure/ApplicationInsightsMonitor.cs deleted file mode 100644 index 1892f5286..000000000 --- a/source/Server/Integration/Azure/ApplicationInsightsMonitor.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.ApplicationInsights.Metrics; -using MirrorSharp.Advanced; -using MirrorSharp.Internal; -using Newtonsoft.Json; -using SharpLab.Server.MirrorSharp; -using SharpLab.Server.Monitoring; - -namespace SharpLab.Server.Integration.Azure { - public class ApplicationInsightsMonitor : IMonitor { - private static readonly ConcurrentDictionary _metricIdentifiers = new(); - - private readonly TelemetryClient _client; - private readonly string _webAppName; - - public ApplicationInsightsMonitor(TelemetryClient client, string webAppName) { - _client = Argument.NotNull(nameof(client), client); - _webAppName = Argument.NotNullOrEmpty(nameof(webAppName), webAppName); - } - - public void Metric(MonitorMetric metric, double value) { - var identifier = _metricIdentifiers.GetOrAdd(metric, static m => new(m.Namespace, m.Name)); - _client.GetMetric(identifier).TrackValue(value); - } - - public void Exception(Exception exception, IWorkSession? session, IDictionary? extras = null) { - var sessionInternals = session as WorkSession; - var telemetry = new ExceptionTelemetry(exception) { - Context = { Session = { Id = session?.GetSessionId() } }, - Properties = { - { "Web App", _webAppName }, - { "Code", session?.GetText() }, - { "Language", session?.LanguageName }, - { "Target", session?.GetTargetName() }, - { "Cursor", sessionInternals?.CursorPosition.ToString() }, - { "Completion", FormatCompletion(sessionInternals) } - } - }; - if (extras != null) { - foreach (var pair in extras) { - telemetry.Properties.Add(pair.Key, pair.Value); - } - } - _client.TrackException(telemetry); - } - - private string? FormatCompletion(WorkSession? session) { - try { - if (session == null) - return null; - - var current = session.CurrentCompletion; - if (current.List == null && !current.ChangeEchoPending && current.PendingChar == null) - return null; - - return JsonConvert.ToString(new { - List = current.List is { } list ? new { - Items = new { - Take10 = list.ItemsList.Take(10), - list.ItemsList.Count - }, - list.Span - } : null, - current.ChangeEchoPending, - current.PendingChar - }); - } - catch (Exception ex) { - return ""; - } - } - } -} diff --git a/source/Server/Integration/Azure/AzureBlobResultCacheStore.cs b/source/Server/Integration/Azure/AzureBlobResultCacheStore.cs index a66663725..d9ffe8dfe 100644 --- a/source/Server/Integration/Azure/AzureBlobResultCacheStore.cs +++ b/source/Server/Integration/Azure/AzureBlobResultCacheStore.cs @@ -5,57 +5,55 @@ using Azure.Storage.Blobs; using SharpLab.Server.Caching.Internal; using System; -using SharpLab.Server.Monitoring; using SharpLab.Server.Caching; -namespace SharpLab.Server.Integration.Azure { - public class AzureBlobResultCacheStore : IResultCacheStore, IDisposable { - private readonly MemoryCache _alreadyCached = new(new MemoryCacheOptions()); - private readonly BlobContainerClient _containerClient; - private readonly string _cachePathPrefix; - private readonly IMonitor _monitor; - - public AzureBlobResultCacheStore( - BlobContainerClient containerClient, - string cachePathPrefix, - IMonitor monitor - ) { - _containerClient = containerClient; - _cachePathPrefix = cachePathPrefix; - _monitor = monitor; - } +namespace SharpLab.Server.Integration.Azure; +public class AzureBlobResultCacheStore : IResultCacheStore, IDisposable { + private readonly MemoryCache _alreadyCached = new(new MemoryCacheOptions()); + private readonly BlobContainerClient _containerClient; + private readonly string _cachePathPrefix; + private readonly ICachingTracker _cachingTracker; + + public AzureBlobResultCacheStore( + BlobContainerClient containerClient, + string cachePathPrefix, + ICachingTracker cachingTracker + ) { + _containerClient = containerClient; + _cachePathPrefix = cachePathPrefix; + _cachingTracker = cachingTracker; + } - public Task StoreAsync(string key, Stream stream, CancellationToken cancellationToken) { - Argument.NotNullOrEmpty(nameof(key), key); - Argument.NotNull(nameof(stream), stream); - - // no need to retry if we already processed this one - if (_alreadyCached.TryGetValue(key, out _)) - return Task.CompletedTask; - - // it's OK to do this before trying, if we failed before we don't want to retry either - var currentCall = new object(); - if (_alreadyCached.GetOrCreate(key, e => Cache(e, currentCall)) != currentCall) - return Task.CompletedTask; - - _monitor.Metric(CachingMetrics.BlobUploadRequestCount, 1); - var path = $"{_cachePathPrefix}/{key}.json"; - try { - return _containerClient.UploadBlobAsync(path, stream, cancellationToken); - } - catch { - _monitor.Metric(CachingMetrics.BlobUploadErrorCount, 1); - throw; - } - } + public Task StoreAsync(string key, Stream stream, CancellationToken cancellationToken) { + Argument.NotNullOrEmpty(nameof(key), key); + Argument.NotNull(nameof(stream), stream); - private object Cache(ICacheEntry entry, object value) { - entry.SlidingExpiration = TimeSpan.FromDays(1); - return value; - } + // no need to retry if we already processed this one + if (_alreadyCached.TryGetValue(key, out _)) + return Task.CompletedTask; + + // it's OK to do this before trying, if we failed before we don't want to retry either + var currentCall = new object(); + if (_alreadyCached.GetOrCreate(key, e => Cache(e, currentCall)) != currentCall) + return Task.CompletedTask; - public void Dispose() { - _alreadyCached.Dispose(); + _cachingTracker.TrackBlobUploadRequest(); + var path = $"{_cachePathPrefix}/{key}.json"; + try { + return _containerClient.UploadBlobAsync(path, stream, cancellationToken); } + catch { + _cachingTracker.TrackBlobUploadError(); + throw; + } + } + + private object Cache(ICacheEntry entry, object value) { + entry.SlidingExpiration = TimeSpan.FromDays(1); + return value; + } + + public void Dispose() { + _alreadyCached.Dispose(); } } diff --git a/source/Server/Integration/Azure/AzureModule.cs b/source/Server/Integration/Azure/AzureModule.cs index 399c718d1..3789c7301 100644 --- a/source/Server/Integration/Azure/AzureModule.cs +++ b/source/Server/Integration/Azure/AzureModule.cs @@ -7,93 +7,107 @@ using JetBrains.Annotations; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights.Metrics; using Microsoft.Azure.Cosmos.Table; using SharpLab.Server.Caching.Internal; using SharpLab.Server.Common; using SharpLab.Server.Monitoring; -namespace SharpLab.Server.Integration.Azure { - [UsedImplicitly] - public class AzureModule : Module { - protected override void Load(ContainerBuilder builder) { - // This is available even on local (through a mock) - RegisterCacheStore(builder); - - var keyVaultUrl = Environment.GetEnvironmentVariable("SHARPLAB_KEY_VAULT_URL"); - if (keyVaultUrl == null) - return; - - RegisterKeyVault(builder, keyVaultUrl); - RegisterTableStorage(builder); - RegisterApplicationInsights(builder); - } - - private void RegisterCacheStore(ContainerBuilder builder) { - const string cacheClientName = "BlobContainerClient-CacheClient"; - var cachePathPrefix = EnvironmentHelper.GetRequiredEnvironmentVariable("SHARPLAB_CACHE_PATH_PREFIX"); - - builder - .Register(c => { - var connectionString = c.Resolve().GetSecret("PublicStorageConnectionString"); - return new BlobContainerClient(connectionString, "cache"); - }) - .Named(cacheClientName) - .SingleInstance(); - - builder - .RegisterType() - .As() - .SingleInstance() - .WithParameter("cachePathPrefix", cachePathPrefix) - .WithParameter(new ResolvedParameter( - (p, c) => p.ParameterType == typeof(BlobContainerClient), - (p, c) => c.ResolveNamed(cacheClientName) - )); - } - - private void RegisterKeyVault(ContainerBuilder builder, string keyVaultUrl) { - var secretClient = new SecretClient(new Uri(keyVaultUrl), new ManagedIdentityCredential()); - builder.RegisterInstance(secretClient) - .AsSelf(); - - builder.RegisterType() - .As() - .SingleInstance(); - } - - private void RegisterTableStorage(ContainerBuilder builder) { - builder.Register(c => { - var connectionString = c.Resolve().GetSecret("StorageConnectionString"); - return CloudStorageAccount.Parse(connectionString).CreateCloudTableClient(); - }).AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .WithParameter("flagKeys", new[] { "ContainerExperimentRollout" }) - .WithParameter(new ResolvedParameter( - (p, _) => p.ParameterType == typeof(CloudTable), - (_, c) => c.Resolve().GetTableReference("featureflags") - )) - .SingleInstance(); - - builder.RegisterBuildCallback(c => c.Resolve().Start()); - } - - private void RegisterApplicationInsights(ContainerBuilder builder) { - builder.Register(c => { - var instrumentationKey = c.Resolve().GetSecret("AppInsightsInstrumentationKey"); - var configuration = new TelemetryConfiguration { InstrumentationKey = instrumentationKey }; - return new TelemetryClient(configuration); - }).AsSelf() - .SingleInstance(); - - var webAppName = EnvironmentHelper.GetRequiredEnvironmentVariable("SHARPLAB_WEBAPP_NAME"); - builder.RegisterType() - .As() - .WithParameter("webAppName", webAppName) - .SingleInstance(); - } +namespace SharpLab.Server.Integration.Azure; + +[UsedImplicitly] +public class AzureModule : Module { + protected override void Load(ContainerBuilder builder) { + // This is available even on local (through a mock) + RegisterCacheStore(builder); + + var keyVaultUrl = Environment.GetEnvironmentVariable("SHARPLAB_KEY_VAULT_URL"); + if (keyVaultUrl == null) + return; + + RegisterKeyVault(builder, keyVaultUrl); + RegisterTableStorage(builder); + RegisterApplicationInsights(builder); + } + + private void RegisterCacheStore(ContainerBuilder builder) { + const string cacheClientName = "BlobContainerClient-CacheClient"; + var cachePathPrefix = EnvironmentHelper.GetRequiredEnvironmentVariable("SHARPLAB_CACHE_PATH_PREFIX"); + + builder + .Register(c => { + var connectionString = c.Resolve().GetSecret("PublicStorageConnectionString"); + return new BlobContainerClient(connectionString, "cache"); + }) + .Named(cacheClientName) + .SingleInstance(); + + builder + .RegisterType() + .As() + .SingleInstance() + .WithParameter("cachePathPrefix", cachePathPrefix) + .WithParameter(new ResolvedParameter( + (p, c) => p.ParameterType == typeof(BlobContainerClient), + (p, c) => c.ResolveNamed(cacheClientName) + )); + } + + private void RegisterKeyVault(ContainerBuilder builder, string keyVaultUrl) { + var secretClient = new SecretClient(new Uri(keyVaultUrl), new ManagedIdentityCredential()); + builder.RegisterInstance(secretClient) + .AsSelf(); + + builder.RegisterType() + .As() + .SingleInstance(); + } + + private void RegisterTableStorage(ContainerBuilder builder) { + builder.Register(c => { + var connectionString = c.Resolve().GetSecret("StorageConnectionString"); + return CloudStorageAccount.Parse(connectionString).CreateCloudTableClient(); + }).AsSelf() + .SingleInstance(); + + builder.RegisterType() + .As() + .AsSelf() + .WithParameter("flagKeys", new[] { "ContainerExperimentRollout" }) + .WithParameter(new ResolvedParameter( + (p, _) => p.ParameterType == typeof(CloudTable), + (_, c) => c.Resolve().GetTableReference("featureflags") + )) + .SingleInstance(); + + builder.RegisterBuildCallback(c => c.Resolve().Start()); + } + + private void RegisterApplicationInsights(ContainerBuilder builder) { + builder.Register(c => { + var instrumentationKey = c.Resolve().GetSecret("AppInsightsInstrumentationKey"); + var configuration = new TelemetryConfiguration { InstrumentationKey = instrumentationKey }; + return new TelemetryClient(configuration); + }).AsSelf() + .SingleInstance(); + + var webAppName = EnvironmentHelper.GetRequiredEnvironmentVariable("SHARPLAB_WEBAPP_NAME"); + builder.RegisterType() + .As() + .WithParameter("webAppName", webAppName) + .SingleInstance(); + + builder.RegisterType() + .AsSelf() + .InstancePerDependency(); + + builder.Register(c => { + var client = c.Resolve(); + var createMonitor = c.Resolve>(); + return (@namespace, name) => { + var metric = client.GetMetric(new MetricIdentifier(@namespace, name)); + return createMonitor(metric); + }; + }).SingleInstance(); } } \ No newline at end of file diff --git a/source/Server/MirrorSharp/ConnectionSendViewer.cs b/source/Server/MirrorSharp/ConnectionSendViewer.cs index 116e8e774..da298f9e5 100644 --- a/source/Server/MirrorSharp/ConnectionSendViewer.cs +++ b/source/Server/MirrorSharp/ConnectionSendViewer.cs @@ -5,59 +5,57 @@ using MirrorSharp.Advanced.EarlyAccess; using SharpLab.Server.Caching; using SharpLab.Server.Execution; -using SharpLab.Server.Monitoring; - -namespace SharpLab.Server.MirrorSharp { - public class ConnectionSendViewer : IConnectionSendViewer { - private readonly IResultCacher _cacher; - private readonly IExceptionLogger _exceptionLogger; - private readonly IMonitor _monitor; - - public ConnectionSendViewer(IResultCacher cacher, IExceptionLogger exceptionLogger, IMonitor monitor) { - _cacher = cacher; - _exceptionLogger = exceptionLogger; - _monitor = monitor; - } - public Task ViewDuringSendAsync(string messageTypeName, ReadOnlyMemory message, IWorkSession session, CancellationToken cancellationToken) { - if (messageTypeName != "slowUpdate") - return Task.CompletedTask; +namespace SharpLab.Server.MirrorSharp; +public class ConnectionSendViewer : IConnectionSendViewer { + private readonly IResultCacher _cacher; + private readonly IExceptionLogger _exceptionLogger; + private readonly ICachingTracker _tracker; - if (session.HasCachingSeenSlowUpdateBefore()) - return Task.CompletedTask; + public ConnectionSendViewer(IResultCacher cacher, IExceptionLogger exceptionLogger, ICachingTracker tracker) { + _cacher = cacher; + _exceptionLogger = exceptionLogger; + _tracker = tracker; + } - // if update should not be cached, we will still not want to cache or measure the next one - session.SetCachingHasSeenSlowUpdate(); + public Task ViewDuringSendAsync(string messageTypeName, ReadOnlyMemory message, IWorkSession session, CancellationToken cancellationToken) { + if (messageTypeName != "slowUpdate") + return Task.CompletedTask; - if (session.IsCachingDisabled()) { - _monitor.Metric(CachingMetrics.NoCacheRequestCount, 1); - return Task.CompletedTask; - } + if (session.HasCachingSeenSlowUpdateBefore()) + return Task.CompletedTask; - if (!ShouldCache(session.GetLastSlowUpdateResult())) - return Task.CompletedTask; + // if update should not be cached, we will still not want to cache or measure the next one + session.SetCachingHasSeenSlowUpdate(); - _monitor.Metric(CachingMetrics.CacheableRequestCount, 1); - return SafeCacheAsync(message, session, cancellationToken); + if (session.IsCachingDisabled()) { + _tracker.TrackNoCacheRequest(); + return Task.CompletedTask; } - private bool ShouldCache(object? result) { - return result is not ContainerExecutionResult { OutputFailed: true }; - } + if (!ShouldCache(session.GetLastSlowUpdateResult())) + return Task.CompletedTask; + + _tracker.TrackCacheableRequest(); + return SafeCacheAsync(message, session, cancellationToken); + } + + private bool ShouldCache(object? result) { + return result is not ContainerExecutionResult { OutputFailed: true }; + } - private async Task SafeCacheAsync(ReadOnlyMemory message, IWorkSession session, CancellationToken cancellationToken) { - try { - var key = new ResultCacheKeyData( - session.LanguageName, - session.GetTargetName()!, - session.GetOptimize()!, - session.GetText() - ); - await _cacher.CacheAsync(key, message, cancellationToken); - } - catch (Exception ex) { - _exceptionLogger.LogException(ex, session); - } + private async Task SafeCacheAsync(ReadOnlyMemory message, IWorkSession session, CancellationToken cancellationToken) { + try { + var key = new ResultCacheKeyData( + session.LanguageName, + session.GetTargetName()!, + session.GetOptimize()!, + session.GetText() + ); + await _cacher.CacheAsync(key, message, cancellationToken); + } + catch (Exception ex) { + _exceptionLogger.LogException(ex, session); } } } diff --git a/source/Server/MirrorSharp/MonitorExceptionLogger.cs b/source/Server/MirrorSharp/MonitorExceptionLogger.cs index 9c7b32398..0b37b93b2 100644 --- a/source/Server/MirrorSharp/MonitorExceptionLogger.cs +++ b/source/Server/MirrorSharp/MonitorExceptionLogger.cs @@ -1,23 +1,21 @@ using System; -using System.Net.WebSockets; using MirrorSharp.Advanced; using SharpLab.Server.Common; using SharpLab.Server.Monitoring; -namespace SharpLab.Server.MirrorSharp { - public class MonitorExceptionLogger : IExceptionLogger { - private readonly IExceptionLogFilter _filter; - private readonly IMonitor _monitor; +namespace SharpLab.Server.MirrorSharp; +public class MonitorExceptionLogger : IExceptionLogger { + private readonly IExceptionLogFilter _filter; + private readonly IExceptionMonitor _monitor; - public MonitorExceptionLogger(IExceptionLogFilter filter, IMonitor monitor) { - _filter = filter; - _monitor = monitor; - } + public MonitorExceptionLogger(IExceptionLogFilter filter, IExceptionMonitor monitor) { + _filter = filter; + _monitor = monitor; + } - public void LogException(Exception exception, IWorkSession session) { - if (!_filter.ShouldLog(exception, session)) - return; - _monitor.Exception(exception, session); - } + public void LogException(Exception exception, IWorkSession session) { + if (!_filter.ShouldLog(exception, session)) + return; + _monitor.Exception(exception, session); } } diff --git a/source/Server/MirrorSharp/SetOptionsFromClient.cs b/source/Server/MirrorSharp/SetOptionsFromClient.cs index a3971b013..8db350aaa 100644 --- a/source/Server/MirrorSharp/SetOptionsFromClient.cs +++ b/source/Server/MirrorSharp/SetOptionsFromClient.cs @@ -6,43 +6,43 @@ using SharpLab.Server.Caching; using SharpLab.Server.Common; -namespace SharpLab.Server.MirrorSharp { - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public class SetOptionsFromClient : ISetOptionsFromClientExtension { - private const string Optimize = "x-optimize"; - private const string Target = "x-target"; - private const string NoCache = "x-no-cache"; +namespace SharpLab.Server.MirrorSharp; - private readonly IDictionary _languages; +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public class SetOptionsFromClient : ISetOptionsFromClientExtension { + private const string Optimize = "x-optimize"; + private const string Target = "x-target"; + private const string NoCache = "x-no-cache"; - public SetOptionsFromClient(IReadOnlyList languages) { - _languages = languages.ToDictionary(l => l.LanguageName); - } + private readonly IDictionary _languages; + + public SetOptionsFromClient(IReadOnlyList languages) { + _languages = languages.ToDictionary(l => l.LanguageName); + } - public bool TrySetOption(IWorkSession session, string name, string value) { - switch (name) { - case Optimize: - session.SetOptimize(value); - _languages[session.LanguageName].SetOptimize(session, value); - return true; - case Target: - session.SetTargetName(value); - _languages[session.LanguageName].SetOptionsForTarget(session, value); - return true; - case NoCache: - if (value != "true") - throw new NotSupportedException("Option 'no-cache' can only be set to true."); - // Mostly used to avoid caching on the first change after a cached result was loaded - session.SetCachingDisabled(true); - return true; - default: - #if !DEBUG - // Need to allow unknown options for future compatibility - return true; - #else - return false; - #endif - } + public bool TrySetOption(IWorkSession session, string name, string value) { + switch (name) { + case Optimize: + session.SetOptimize(value); + _languages[session.LanguageName].SetOptimize(session, value); + return true; + case Target: + session.SetTargetName(value); + _languages[session.LanguageName].SetOptionsForTarget(session, value); + return true; + case NoCache: + if (value != "true") + throw new NotSupportedException("Option 'no-cache' can only be set to true."); + // Mostly used to avoid caching on the first change after a cached result was loaded + session.SetCachingDisabled(true); + return true; + default: + #if !DEBUG + // Need to allow unknown options for future compatibility + return true; + #else + return false; + #endif } } } \ No newline at end of file diff --git a/source/Server/MirrorSharp/SlowUpdate.cs b/source/Server/MirrorSharp/SlowUpdate.cs index 1ccacaa26..949c83396 100644 --- a/source/Server/MirrorSharp/SlowUpdate.cs +++ b/source/Server/MirrorSharp/SlowUpdate.cs @@ -16,176 +16,189 @@ using SharpLab.Server.Decompilation; using SharpLab.Server.Decompilation.AstOnly; using SharpLab.Server.Execution; -using SharpLab.Server.Execution.Container; using SharpLab.Server.Explanation; using SharpLab.Server.Monitoring; using LanguageNames = SharpLab.Server.Common.LanguageNames; -namespace SharpLab.Server.MirrorSharp { - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public class SlowUpdate : ISlowUpdateExtension { - private readonly ICSharpTopLevelProgramSupport _topLevelProgramSupport; - private readonly ICompiler _compiler; - private readonly IReadOnlyDictionary _decompilers; - private readonly IReadOnlyDictionary _astTargets; - private readonly IContainerExecutor _containerExecutor; - private readonly IExplainer _explainer; - private readonly RecyclableMemoryStreamManager _memoryStreamManager; - private readonly IMonitor _monitor; - - public SlowUpdate( - ICSharpTopLevelProgramSupport topLevelProgramSupport, - ICompiler compiler, - IReadOnlyCollection decompilers, - IReadOnlyCollection astTargets, - IContainerExecutor containerExecutor, - IExplainer explainer, - RecyclableMemoryStreamManager memoryStreamManager, - IMonitor monitor - ) { - _topLevelProgramSupport = topLevelProgramSupport; - _compiler = compiler; - _decompilers = decompilers.ToDictionary(d => d.LanguageName); - _astTargets = astTargets - .SelectMany(t => t.SupportedLanguageNames.Select(n => (target: t, languageName: n))) - .ToDictionary(x => x.languageName, x => x.target); - _containerExecutor = containerExecutor; - _memoryStreamManager = memoryStreamManager; - _monitor = monitor; - _explainer = explainer; - } +namespace SharpLab.Server.MirrorSharp; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public class SlowUpdate : ISlowUpdateExtension { + private readonly ICSharpTopLevelProgramSupport _topLevelProgramSupport; + private readonly ICompiler _compiler; + private readonly IReadOnlyDictionary _decompilers; + private readonly IReadOnlyDictionary _astTargets; + private readonly IContainerExecutor _containerExecutor; + private readonly IExplainer _explainer; + private readonly RecyclableMemoryStreamManager _memoryStreamManager; + private readonly IFeatureTracker _featureTracker; + private readonly IExceptionMonitor _exceptionMonitor; + private readonly IMetricMonitor _containerRunCountMonitor; + private readonly IMetricMonitor _containerFailureCountMonitor; + + public SlowUpdate( + ICSharpTopLevelProgramSupport topLevelProgramSupport, + ICompiler compiler, + IReadOnlyCollection decompilers, + IReadOnlyCollection astTargets, + IContainerExecutor containerExecutor, + IExplainer explainer, + RecyclableMemoryStreamManager memoryStreamManager, + IFeatureTracker featureTracker, + MetricMonitorFactory createMetricMonitor, + IExceptionMonitor exceptionMonitor + ) { + _topLevelProgramSupport = topLevelProgramSupport; + _compiler = compiler; + _decompilers = decompilers.ToDictionary(d => d.LanguageName); + _astTargets = astTargets + .SelectMany(t => t.SupportedLanguageNames.Select(n => (target: t, languageName: n))) + .ToDictionary(x => x.languageName, x => x.target); + _containerExecutor = containerExecutor; + _memoryStreamManager = memoryStreamManager; + _explainer = explainer; + _featureTracker = featureTracker; + _exceptionMonitor = exceptionMonitor; + _containerRunCountMonitor = createMetricMonitor("container-experiment", "Runs: Container"); + _containerFailureCountMonitor = createMetricMonitor("container-experiment", "Runs: Failed"); + } - public async Task ProcessAsync(IWorkSession session, IList diagnostics, CancellationToken cancellationToken) { - //AssemblyLog.Enable(n => $"assembly/{n}.dll"); - PerformanceLog.Checkpoint("SlowUpdate.ProcessAsync.Start"); - var targetName = GetAndEnsureTargetName(session); + public async Task ProcessAsync(IWorkSession session, IList diagnostics, CancellationToken cancellationToken) { + //AssemblyLog.Enable(n => $"assembly/{n}.dll"); + PerformanceLog.Checkpoint("SlowUpdate.ProcessAsync.Start"); + _featureTracker.TrackBranch(); - _topLevelProgramSupport.UpdateOutputKind(session, diagnostics); + var targetName = GetAndEnsureTargetName(session); + _featureTracker.TrackLanguage(session.LanguageName); + _featureTracker.TrackTarget(targetName); - if (targetName is TargetNames.Ast or TargetNames.Explain) { - if (session.LanguageName == LanguageNames.IL) - throw new NotSupportedException($"Target '{targetName}' is not (yet?) supported for IL."); + _topLevelProgramSupport.UpdateOutputKind(session, diagnostics); - var astTarget = _astTargets[session.LanguageName]; - var ast = await astTarget.GetAstAsync(session, cancellationToken).ConfigureAwait(false); - if (targetName == TargetNames.Explain) - return await _explainer.ExplainAsync(ast, session, cancellationToken).ConfigureAwait(false); - return ast; - } + if (targetName is TargetNames.Ast or TargetNames.Explain) { + if (session.LanguageName == LanguageNames.IL) + throw new NotSupportedException($"Target '{targetName}' is not (yet?) supported for IL."); - if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) - return null; + var astTarget = _astTargets[session.LanguageName]; + var ast = await astTarget.GetAstAsync(session, cancellationToken).ConfigureAwait(false); + if (targetName == TargetNames.Explain) + return await _explainer.ExplainAsync(ast, session, cancellationToken).ConfigureAwait(false); + return ast; + } - if (targetName == LanguageNames.VisualBasic) - return VisualBasicNotAvailable; - - if (targetName is not (TargetNames.Run or TargetNames.Verify) && !_decompilers.ContainsKey(targetName)) - throw new NotSupportedException($"Target '{targetName}' is not (yet?) supported by this branch."); - - MemoryStream? assemblyStream = null; - MemoryStream? symbolStream = null; - try { - assemblyStream = _memoryStreamManager.GetStream(); - if (targetName is TargetNames.Run or TargetNames.IL or TargetNames.RunIL) - symbolStream = _memoryStreamManager.GetStream(); - - var compilationStopwatch = session.ShouldReportPerformance() ? Stopwatch.StartNew() : null; - var compiled = await _compiler.TryCompileToStreamAsync(assemblyStream, symbolStream, session, diagnostics, cancellationToken).ConfigureAwait(false); - compilationStopwatch?.Stop(); - if (!compiled.assembly) { - assemblyStream.Dispose(); - symbolStream?.Dispose(); - return null; - } + if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) + return null; - if (targetName == TargetNames.Verify) { - assemblyStream.Dispose(); - symbolStream?.Dispose(); - return "✔️ Compilation completed."; - } + if (targetName == LanguageNames.VisualBasic) + return VisualBasicNotAvailable; - assemblyStream.Seek(0, SeekOrigin.Begin); - symbolStream?.Seek(0, SeekOrigin.Begin); - #if DEBUG - DiagnosticLog.LogAssembly("1.Compiled", assemblyStream, compiled.symbols ? symbolStream : null); - #endif - - var streams = new CompilationStreamPair(assemblyStream, compiled.symbols ? symbolStream : null); - if (targetName == TargetNames.Run) { - try { - var result = await _containerExecutor.ExecuteAsync(streams, session, cancellationToken); - if (compilationStopwatch != null) { - // TODO: Prettify - // output += $"\n COMPILATION: {compilationStopwatch.ElapsedMilliseconds,15}ms"; - } - streams.Dispose(); - _monitor.Metric(ContainerExperimentMetrics.ContainerRunCount, 1); - return result; - } - catch { - _monitor.Metric(ContainerExperimentMetrics.ContainerFailureCount, 1); - throw; - } - } + if (targetName is not (TargetNames.Run or TargetNames.Verify) && !_decompilers.ContainsKey(targetName)) + throw new NotSupportedException($"Target '{targetName}' is not (yet?) supported by this branch."); - // it's fine not to Dispose() here -- MirrorSharp will dispose it after calling WriteResult() - return streams; + _featureTracker.TrackOptimize(session.GetOptimize()); + + MemoryStream? assemblyStream = null; + MemoryStream? symbolStream = null; + try { + assemblyStream = _memoryStreamManager.GetStream(); + if (targetName is TargetNames.Run or TargetNames.IL or TargetNames.RunIL) + symbolStream = _memoryStreamManager.GetStream(); + + var compilationStopwatch = session.ShouldReportPerformance() ? Stopwatch.StartNew() : null; + var compiled = await _compiler.TryCompileToStreamAsync(assemblyStream, symbolStream, session, diagnostics, cancellationToken).ConfigureAwait(false); + compilationStopwatch?.Stop(); + if (!compiled.assembly) { + assemblyStream.Dispose(); + symbolStream?.Dispose(); + return null; } - catch { - assemblyStream?.Dispose(); + + if (targetName == TargetNames.Verify) { + assemblyStream.Dispose(); symbolStream?.Dispose(); - throw; + return "✔️ Compilation completed."; } - } - public void WriteResult(IFastJsonWriter writer, object? result, IWorkSession session) { - session.SetLastSlowUpdateResult(result); + assemblyStream.Seek(0, SeekOrigin.Begin); + symbolStream?.Seek(0, SeekOrigin.Begin); + #if DEBUG + DiagnosticLog.LogAssembly("1.Compiled", assemblyStream, compiled.symbols ? symbolStream : null); + #endif - if (result == null) { - writer.WriteValue((string?)null); - return; + var streams = new CompilationStreamPair(assemblyStream, compiled.symbols ? symbolStream : null); + if (targetName == TargetNames.Run) { + try { + var result = await _containerExecutor.ExecuteAsync(streams, session, cancellationToken); + if (compilationStopwatch != null) { + // TODO: Prettify + // output += $"\n COMPILATION: {compilationStopwatch.ElapsedMilliseconds,15}ms"; + } + streams.Dispose(); + _containerRunCountMonitor.Track(1); + return result; + } + catch { + _containerFailureCountMonitor.Track(1); + throw; + } } - if (result is string s) { - writer.WriteValue(s); - return; - } + // it's fine not to Dispose() here -- MirrorSharp will dispose it after calling WriteResult() + return streams; + } + catch { + assemblyStream?.Dispose(); + symbolStream?.Dispose(); + throw; + } + } - var targetName = GetAndEnsureTargetName(session); - if (targetName == TargetNames.Ast) { - var astTarget = _astTargets[session.LanguageName]; - astTarget.SerializeAst(result, writer, session); - return; - } + public void WriteResult(IFastJsonWriter writer, object? result, IWorkSession session) { + session.SetLastSlowUpdateResult(result); - if (targetName == TargetNames.Explain) { - _explainer.Serialize((ExplanationResult)result, writer); - return; - } + if (result == null) { + writer.WriteValue((string?)null); + return; + } - if (targetName == TargetNames.Run) { - writer.WriteValue(((ContainerExecutionResult)result).Output); - return; - } + if (result is string s) { + writer.WriteValue(s); + return; + } - var decompiler = _decompilers[targetName]; - using (var streams = (CompilationStreamPair)result) - using (var stringWriter = writer.OpenString()) { - decompiler.Decompile(streams, stringWriter, session); - } + var targetName = GetAndEnsureTargetName(session); + if (targetName == TargetNames.Ast) { + var astTarget = _astTargets[session.LanguageName]; + astTarget.SerializeAst(result, writer, session); + return; + } + + if (targetName == TargetNames.Explain) { + _explainer.Serialize((ExplanationResult)result, writer); + return; } - private const string VisualBasicNotAvailable = - "' Unfortunately, Visual Basic decompilation is no longer supported.\r\n" + - "' \r\n" + - "' All decompilation in SharpLab is provided by ILSpy, and latest ILSpy does not suport VB.\r\n" + - "' If you are interested in VB, please discuss or contribute at https://github.com/icsharpcode/ILSpy."; - - private string GetAndEnsureTargetName(IWorkSession session) { - var targetName = session.GetTargetName(); - if (targetName == null) - throw new InvalidOperationException("Target is not set on the session (timing issue?). Please try reloading."); - return targetName; + if (targetName == TargetNames.Run) { + writer.WriteValue(((ContainerExecutionResult)result).Output); + return; } + + var decompiler = _decompilers[targetName]; + using (var streams = (CompilationStreamPair)result) + using (var stringWriter = writer.OpenString()) { + decompiler.Decompile(streams, stringWriter, session); + } + } + + private const string VisualBasicNotAvailable = + "' Unfortunately, Visual Basic decompilation is no longer supported.\r\n" + + "' \r\n" + + "' All decompilation in SharpLab is provided by ILSpy, and latest ILSpy does not suport VB.\r\n" + + "' If you are interested in VB, please discuss or contribute at https://github.com/icsharpcode/ILSpy."; + + private string GetAndEnsureTargetName(IWorkSession session) { + var targetName = session.GetTargetName(); + if (targetName == null) + throw new InvalidOperationException("Target is not set on the session (timing issue?). Please try reloading."); + return targetName; } } \ No newline at end of file diff --git a/source/Server/MirrorSharp/WorkSessionExtensions.cs b/source/Server/MirrorSharp/WorkSessionExtensions.cs index 2c4bad6f9..3a5ce0474 100644 --- a/source/Server/MirrorSharp/WorkSessionExtensions.cs +++ b/source/Server/MirrorSharp/WorkSessionExtensions.cs @@ -2,47 +2,47 @@ using AshMind.Extensions; using MirrorSharp.Advanced; -namespace SharpLab.Server.MirrorSharp { - public static class WorkSessionExtensions { - public static string? GetTargetName(this IWorkSession session) { - return (string?)session.ExtensionData.GetValueOrDefault("TargetName"); - } +namespace SharpLab.Server.MirrorSharp; - public static void SetTargetName(this IWorkSession session, string value) { - session.ExtensionData["TargetName"] = value; - } +public static class WorkSessionExtensions { + public static string? GetTargetName(this IWorkSession session) { + return (string?)session.ExtensionData.GetValueOrDefault("TargetName"); + } - public static string? GetOptimize(this IWorkSession session) { - return (string?)session.ExtensionData.GetValueOrDefault("Optimize"); - } + public static void SetTargetName(this IWorkSession session, string value) { + session.ExtensionData["TargetName"] = value; + } - public static void SetOptimize(this IWorkSession session, string value) { - session.ExtensionData["Optimize"] = value; - } + public static string? GetOptimize(this IWorkSession session) { + return (string?)session.ExtensionData.GetValueOrDefault("Optimize"); + } - public static bool ShouldReportPerformance(this IWorkSession session) { - return (bool?)session.ExtensionData.GetValueOrDefault("DebugIncludePerformance") ?? false; - } + public static void SetOptimize(this IWorkSession session, string value) { + session.ExtensionData["Optimize"] = value; + } - public static void SetShouldReportPerformance(this IWorkSession session, bool value) { - session.ExtensionData["DebugIncludePerformance"] = value; - } + public static bool ShouldReportPerformance(this IWorkSession session) { + return (bool?)session.ExtensionData.GetValueOrDefault("DebugIncludePerformance") ?? false; + } - public static object? GetLastSlowUpdateResult(this IWorkSession session) { - return session.ExtensionData.GetValueOrDefault("LastSlowUpdateResult"); - } + public static void SetShouldReportPerformance(this IWorkSession session, bool value) { + session.ExtensionData["DebugIncludePerformance"] = value; + } - public static void SetLastSlowUpdateResult(this IWorkSession session, object? value) { - session.ExtensionData["LastSlowUpdateResult"] = value; - } + public static object? GetLastSlowUpdateResult(this IWorkSession session) { + return session.ExtensionData.GetValueOrDefault("LastSlowUpdateResult"); + } + + public static void SetLastSlowUpdateResult(this IWorkSession session, object? value) { + session.ExtensionData["LastSlowUpdateResult"] = value; + } - public static string GetSessionId(this IWorkSession session) { - var id = (string?)session.ExtensionData.GetValueOrDefault("SessionId"); - if (id == null) { - id = Guid.NewGuid().ToString(); - session.ExtensionData["SessionId"] = id; - } - return id; + public static string GetSessionId(this IWorkSession session) { + var id = (string?)session.ExtensionData.GetValueOrDefault("SessionId"); + if (id == null) { + id = Guid.NewGuid().ToString(); + session.ExtensionData["SessionId"] = id; } + return id; } } \ No newline at end of file diff --git a/source/Server/Monitoring/DefaultLoggerExceptionMonitor.cs b/source/Server/Monitoring/DefaultLoggerExceptionMonitor.cs new file mode 100644 index 000000000..6797457f9 --- /dev/null +++ b/source/Server/Monitoring/DefaultLoggerExceptionMonitor.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using MirrorSharp.Advanced; +using SharpLab.Server.MirrorSharp; + +namespace SharpLab.Server.Monitoring; +public class DefaultLoggerExceptionMonitor : IExceptionMonitor { + private readonly ILogger _logger; + + public DefaultLoggerExceptionMonitor(ILogger logger) { + _logger = logger; + } + + public void Exception(Exception exception, IWorkSession? session, IDictionary? extras = null) { + _logger.LogError(exception, "[{SessionId}] Exception: {Message}", session?.GetSessionId(), exception.Message); + } +} diff --git a/source/Server/Monitoring/DefaultLoggerMetricMonitor.cs b/source/Server/Monitoring/DefaultLoggerMetricMonitor.cs new file mode 100644 index 000000000..f03f3d0b1 --- /dev/null +++ b/source/Server/Monitoring/DefaultLoggerMetricMonitor.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; + +namespace SharpLab.Server.Monitoring; + +public class DefaultLoggerMetricMonitor : IMetricMonitor { + private readonly ILogger _logger; + private readonly string _namespace; + private readonly string _name; + + public DefaultLoggerMetricMonitor(ILogger logger, string @namespace, string name) { + Argument.NotNull(nameof(logger), logger); + Argument.NotNullOrEmpty(nameof(@namespace), @namespace); + Argument.NotNullOrEmpty(nameof(name), name); + + _logger = logger; + _namespace = @namespace; + _name = name; + } + + public void Track(double value) { + _logger.LogInformation("Metric {Namespace} {Name}: {Value}.", _namespace, _name, value); + } +} diff --git a/source/Server/Monitoring/DefaultLoggerMonitor.cs b/source/Server/Monitoring/DefaultLoggerMonitor.cs deleted file mode 100644 index aee0478a1..000000000 --- a/source/Server/Monitoring/DefaultLoggerMonitor.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using MirrorSharp.Advanced; -using SharpLab.Server.MirrorSharp; - -namespace SharpLab.Server.Monitoring { - public class DefaultLoggerMonitor : IMonitor { - private readonly ILogger _logger; - - public DefaultLoggerMonitor(ILogger logger) { - _logger = logger; - } - - public void Metric(MonitorMetric metric, double value) { - _logger.LogInformation("Metric {Namespace} {Name}: {Value}.", metric.Namespace, metric.Name, value); - } - - public void Exception(Exception exception, IWorkSession? session, IDictionary? extras = null) { - _logger.LogError(exception, "[{SessionId}] Exception: {Message}", session?.GetSessionId(), exception.Message); - } - } -} diff --git a/source/Server/Monitoring/IExceptionMonitor.cs b/source/Server/Monitoring/IExceptionMonitor.cs new file mode 100644 index 000000000..f990410d6 --- /dev/null +++ b/source/Server/Monitoring/IExceptionMonitor.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; +using MirrorSharp.Advanced; + +namespace SharpLab.Server.Monitoring; +public interface IExceptionMonitor { + void Exception(Exception exception, IWorkSession? session, IDictionary? extras = null); +} diff --git a/source/Server/Monitoring/IMetricMonitor.cs b/source/Server/Monitoring/IMetricMonitor.cs new file mode 100644 index 000000000..750559645 --- /dev/null +++ b/source/Server/Monitoring/IMetricMonitor.cs @@ -0,0 +1,5 @@ +namespace SharpLab.Server.Monitoring; + +public interface IMetricMonitor { + void Track(double value); +} diff --git a/source/Server/Monitoring/IMetricMonitorFactory.cs b/source/Server/Monitoring/IMetricMonitorFactory.cs new file mode 100644 index 000000000..a7f0ef184 --- /dev/null +++ b/source/Server/Monitoring/IMetricMonitorFactory.cs @@ -0,0 +1,3 @@ +namespace SharpLab.Server.Monitoring; + +public delegate IMetricMonitor MetricMonitorFactory(string @namespace, string name); diff --git a/source/Server/Monitoring/IMonitor.cs b/source/Server/Monitoring/IMonitor.cs deleted file mode 100644 index 076562fcc..000000000 --- a/source/Server/Monitoring/IMonitor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using System.Collections.Generic; -using MirrorSharp.Advanced; - -namespace SharpLab.Server.Monitoring { - public interface IMonitor { - void Metric(MonitorMetric metric, double value); - void Exception(Exception exception, IWorkSession? session, IDictionary? extras = null); - } -} diff --git a/source/Server/Monitoring/MonitorMetric.cs b/source/Server/Monitoring/MonitorMetric.cs deleted file mode 100644 index 1eac1b6ec..000000000 --- a/source/Server/Monitoring/MonitorMetric.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace SharpLab.Server.Monitoring { - public class MonitorMetric { - public MonitorMetric(string @namespace, string name) { - Argument.NotNullOrEmpty(nameof(@namespace), @namespace); - Argument.NotNullOrEmpty(nameof(name), name); - - Namespace = @namespace; - Name = name; - } - - public string Namespace { get; } - public string Name { get; } - } -} diff --git a/source/Server/Monitoring/MonitoringModule.cs b/source/Server/Monitoring/MonitoringModule.cs index 62360d86a..a54d12c65 100644 --- a/source/Server/Monitoring/MonitoringModule.cs +++ b/source/Server/Monitoring/MonitoringModule.cs @@ -1,14 +1,27 @@ using Autofac; using JetBrains.Annotations; -namespace SharpLab.Server.Monitoring { - [UsedImplicitly] - public class MonitoringModule : Module { - protected override void Load(ContainerBuilder builder) { - builder.RegisterType() - .As() - .SingleInstance() - .PreserveExistingDefaults(); - } +namespace SharpLab.Server.Monitoring; +[UsedImplicitly] +public class MonitoringModule : Module { + protected override void Load(ContainerBuilder builder) { + builder.RegisterType() + .As() + .SingleInstance() + .PreserveExistingDefaults(); + + builder.RegisterType() + .AsSelf() + .InstancePerDependency(); + + builder.Register(c => { + var context = c.Resolve(); + return (@namespace, name) => context.Resolve( + new NamedParameter("namespace", @namespace), + new NamedParameter("name", name) + ); + }) + .SingleInstance() + .PreserveExistingDefaults(); } } \ No newline at end of file diff --git a/source/Tests/Caching/Unit/AzureBlobResultCacheStoreTests.cs b/source/Tests/Caching/Unit/AzureBlobResultCacheStoreTests.cs index 4b475ce05..aa67fa2a3 100644 --- a/source/Tests/Caching/Unit/AzureBlobResultCacheStoreTests.cs +++ b/source/Tests/Caching/Unit/AzureBlobResultCacheStoreTests.cs @@ -5,22 +5,22 @@ using SourceMock.Internal; using Xunit; using SharpLab.Server.Integration.Azure; -using SharpLab.Server.Monitoring.Mocks; +using SharpLab.Server.Caching.Mocks; -namespace SharpLab.Tests.Caching.Unit { - public class AzureBlobResultCacheStoreTests { - [Fact] - public async Task StoreAsync_DoesNotCallUploadBlobAsync_ForSecondCallWithSameKey() { - // Arrange - var blobContainerMock = new BlobContainerClientMock(); - var store = new AzureBlobResultCacheStore(blobContainerMock, "_", new MonitorMock()); - await store.StoreAsync("test-key", new MemoryStream(), CancellationToken.None); +namespace SharpLab.Tests.Caching.Unit; - // Act - await store.StoreAsync("test-key", new MemoryStream(), CancellationToken.None); +public class AzureBlobResultCacheStoreTests { + [Fact] + public async Task StoreAsync_DoesNotCallUploadBlobAsync_ForSecondCallWithSameKey() { + // Arrange + var blobContainerMock = new BlobContainerClientMock(); + var store = new AzureBlobResultCacheStore(blobContainerMock, "_", new CachingTrackerMock()); + await store.StoreAsync("test-key", new MemoryStream(), CancellationToken.None); - // Assert - Assert.Equal(1, blobContainerMock.Calls.UploadBlobAsync(content: default(MockArgumentMatcher)).Count); - } + // Act + await store.StoreAsync("test-key", new MemoryStream(), CancellationToken.None); + + // Assert + Assert.Equal(1, blobContainerMock.Calls.UploadBlobAsync(content: default(MockArgumentMatcher)).Count); } } diff --git a/source/Tests/Common/Unit/ExceptionLogFilterTests.cs b/source/Tests/Common/Unit/ExceptionLogFilterTests.cs index 3412c3cc0..735e23644 100644 --- a/source/Tests/Common/Unit/ExceptionLogFilterTests.cs +++ b/source/Tests/Common/Unit/ExceptionLogFilterTests.cs @@ -3,33 +3,32 @@ using System; using Xunit; -namespace SharpLab.Tests.Common.Unit { - public class ExceptionLogFilterTests { - [Fact] - public void ShouldLog_ReturnsTrue_ForGeneralException() { - // Arrange - var filter = new ExceptionLogFilter(); +namespace SharpLab.Tests.Common.Unit; +public class ExceptionLogFilterTests { + [Fact] + public void ShouldLog_ReturnsTrue_ForGeneralException() { + // Arrange + var filter = new ExceptionLogFilter(); - // Act - var shouldLog = filter.ShouldLog(new Exception(), new WorkSessionMock()); + // Act + var shouldLog = filter.ShouldLog(new Exception(), new WorkSessionMock()); - // Assert - Assert.True(shouldLog); - } + // Assert + Assert.True(shouldLog); + } - [Fact] - public void ShouldLog_ReturnsFalse_ForBadImageFormatException_WithILEmitByte() { - // Arrange - var filter = new ExceptionLogFilter(); - var session = new WorkSessionMock(); - session.Setup.LanguageName.Returns(LanguageNames.IL); - session.Setup.GetText().Returns("ABC .emitbyte DEF"); + [Fact] + public void ShouldLog_ReturnsFalse_ForBadImageFormatException_WithILEmitByte() { + // Arrange + var filter = new ExceptionLogFilter(); + var session = new WorkSessionMock(); + session.Setup.LanguageName.Returns(LanguageNames.IL); + session.Setup.GetText().Returns("ABC .emitbyte DEF"); - // Act - var shouldLog = filter.ShouldLog(new BadImageFormatException(), session); + // Act + var shouldLog = filter.ShouldLog(new BadImageFormatException(), session); - // Assert - Assert.False(shouldLog); - } + // Assert + Assert.False(shouldLog); } } diff --git a/source/Tests/Common/Unit/LanguageNamesTests.cs b/source/Tests/Common/Unit/LanguageNamesTests.cs new file mode 100644 index 000000000..64b5124aa --- /dev/null +++ b/source/Tests/Common/Unit/LanguageNamesTests.cs @@ -0,0 +1,22 @@ +using Xunit; +using SharpLab.Server.Common; +using System.Linq; +using System.Reflection; + +namespace SharpLab.Tests.Common.Unit; +public class LanguageNamesTests { + [Fact] + public void All_IncludesAllConstants() { + // Arrange + var constants = typeof(LanguageNames) + .GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(f => f.IsLiteral) + .Select(f => (string)f.GetRawConstantValue()!); + + // Act + var all = LanguageNames.All; + + // Assert + Assert.Equal(constants, all); + } +} diff --git a/source/Tests/Common/Unit/TargetNamesTests.cs b/source/Tests/Common/Unit/TargetNamesTests.cs new file mode 100644 index 000000000..c5aefaa27 --- /dev/null +++ b/source/Tests/Common/Unit/TargetNamesTests.cs @@ -0,0 +1,22 @@ +using Xunit; +using SharpLab.Server.Common; +using System.Linq; +using System.Reflection; + +namespace SharpLab.Tests.Common.Unit; +public class TargetNamesTests { + [Fact] + public void All_IncludesAllConstants() { + // Arrange + var constants = typeof(TargetNames) + .GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(f => f.IsLiteral) + .Select(f => (string)f.GetRawConstantValue()!); + + // Act + var all = TargetNames.All; + + // Assert + Assert.Equal(constants, all); + } +} diff --git a/source/Tests/Properties/AssemblyInfo.cs b/source/Tests/Properties/AssemblyInfo.cs index d111d06b6..0a808e81e 100644 --- a/source/Tests/Properties/AssemblyInfo.cs +++ b/source/Tests/Properties/AssemblyInfo.cs @@ -2,16 +2,16 @@ using Microsoft.Extensions.Logging; using MirrorSharp.Advanced; using SharpLab.Container.Manager.Internal; +using SharpLab.Server.Caching; using SharpLab.Server.Caching.Internal; using SharpLab.Server.Execution.Container; -using SharpLab.Server.Monitoring; using SourceMock; [assembly: GenerateMocksForTypes( typeof(IWorkSession), typeof(IRoslynSession), typeof(IDateTimeProvider), - typeof(IMonitor), + typeof(ICachingTracker), typeof(ILogger<>), typeof(IResultCacheStore), typeof(IContainerClient), diff --git a/source/WebApp/app/features/dark-mode/themeState.ts b/source/WebApp/app/features/dark-mode/themeState.ts index ad2444c75..a5963e0ef 100644 --- a/source/WebApp/app/features/dark-mode/themeState.ts +++ b/source/WebApp/app/features/dark-mode/themeState.ts @@ -41,15 +41,4 @@ export const effectiveThemeSelector = selector({ ? get(systemThemeState) : userTheme; } -}); - -/* -function trackDarkTheme(effectiveTheme: EffectiveTheme) { - if (userTheme === 'dark') { - trackFeature('Theme: Dark (manual)'); - } - else if (effectiveTheme === 'dark') { - trackFeature('Theme: Dark (system)'); - } -} -*/ \ No newline at end of file +}); \ No newline at end of file diff --git a/source/WebApp/app/shared/state/targetOptionState.ts b/source/WebApp/app/shared/state/targetOptionState.ts index 7c776d171..ee796b94d 100644 --- a/source/WebApp/app/shared/state/targetOptionState.ts +++ b/source/WebApp/app/shared/state/targetOptionState.ts @@ -4,6 +4,5 @@ import type { TargetName } from '../targets'; export const targetOptionState = atom({ key: 'app-options-target', // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - default: null!, - effects: [] + default: null! }); \ No newline at end of file diff --git a/source/WebApp/app/shared/trackFeature.ts b/source/WebApp/app/shared/trackFeature.ts deleted file mode 100644 index 03c759068..000000000 --- a/source/WebApp/app/shared/trackFeature.ts +++ /dev/null @@ -1,5 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -export const trackFeature = window.appInsights - ? ((feature: string) => { window.appInsights.trackEvent(feature); }) - // eslint-disable-next-line @typescript-eslint/no-empty-function - : (() => {}); \ No newline at end of file