From ce5fe1843a96fe47c84aece8e612eabbc3426b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Fri, 29 Aug 2025 22:52:46 +0300 Subject: [PATCH 1/8] Add ServerState.Empty --- src/CSharpLanguageServer/Lsp/Server.fs | 2 +- src/CSharpLanguageServer/State/ServerState.fs | 32 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/CSharpLanguageServer/Lsp/Server.fs b/src/CSharpLanguageServer/Lsp/Server.fs index 1b1a1462..8401712c 100644 --- a/src/CSharpLanguageServer/Lsp/Server.fs +++ b/src/CSharpLanguageServer/Lsp/Server.fs @@ -37,7 +37,7 @@ type CSharpLspServer( let stateActor = MailboxProcessor.Start( serverEventLoop - { emptyServerState with Settings = settings }) + { ServerState.Empty with Settings = settings }) let getDocumentForUriFromCurrentState docType uri = stateActor.PostAndAsyncReply(fun rc -> GetDocumentOfTypeForUri (docType, uri, rc)) diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index cbb2f0ac..445761b1 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -52,6 +52,23 @@ and ServerState = { PushDiagnosticsDocumentBacklog: string list PushDiagnosticsCurrentDocTask: (string * Task) option } +with + static member Empty = { + Settings = ServerSettings.Default + RootPath = Directory.GetCurrentDirectory() + LspClient = None + ClientCapabilities = emptyClientCapabilities + Solution = None + OpenDocs = Map.empty + DecompiledMetadata = Map.empty + LastRequestId = 0 + RunningRequests = Map.empty + RequestQueue = [] + SolutionReloadPending = None + PushDiagnosticsDocumentBacklog = [] + PushDiagnosticsCurrentDocTask = None + } + let pullFirstRequestMaybe requestQueue = match requestQueue with @@ -83,21 +100,6 @@ let pullNextRequestMaybe requestQueue = (Some nextRequest, queueRemainder) -let emptyServerState = { Settings = ServerSettings.Default - RootPath = Directory.GetCurrentDirectory() - LspClient = None - ClientCapabilities = emptyClientCapabilities - Solution = None - OpenDocs = Map.empty - DecompiledMetadata = Map.empty - LastRequestId = 0 - RunningRequests = Map.empty - RequestQueue = [] - SolutionReloadPending = None - PushDiagnosticsDocumentBacklog = [] - PushDiagnosticsCurrentDocTask = None } - - type ServerDocumentType = | UserDocument // user Document from solution, on disk | DecompiledDocument // Document decompiled from metadata, readonly From aef3b0d49a6dd79e518fdfe67c771ea43c56bb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Sun, 31 Aug 2025 18:31:17 +0300 Subject: [PATCH 2/8] Actually pass in DebugMode to ServerSettings type --- src/CSharpLanguageServer/Program.fs | 1 + src/CSharpLanguageServer/Types.fs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/CSharpLanguageServer/Program.fs b/src/CSharpLanguageServer/Program.fs index 48ef1526..208447cb 100644 --- a/src/CSharpLanguageServer/Program.fs +++ b/src/CSharpLanguageServer/Program.fs @@ -55,6 +55,7 @@ let entry args = ServerSettings.Default with SolutionPath = serverArgs.TryGetResult(<@ CLIArguments.Solution @>) LogLevel = logLevel + DebugMode = debugMode } Logging.setupLogging settings.LogLevel diff --git a/src/CSharpLanguageServer/Types.fs b/src/CSharpLanguageServer/Types.fs index 7fbf867d..24ac2180 100644 --- a/src/CSharpLanguageServer/Types.fs +++ b/src/CSharpLanguageServer/Types.fs @@ -9,11 +9,13 @@ type ServerSettings = { SolutionPath: string option LogLevel: LogLevel ApplyFormattingOptions: bool + DebugMode: bool } static member Default: ServerSettings = { SolutionPath = None LogLevel = LogLevel.Information ApplyFormattingOptions = false + DebugMode = false } type CSharpMetadataInformation = From 2e45ffcbf1a394825801cd64cb0daf565a71f850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Sun, 31 Aug 2025 17:47:07 +0300 Subject: [PATCH 3/8] ServerState.ServerRequestType -> ServerRequestMode --- src/CSharpLanguageServer/Lsp/Server.fs | 4 ++-- src/CSharpLanguageServer/State/ServerState.fs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/CSharpLanguageServer/Lsp/Server.fs b/src/CSharpLanguageServer/Lsp/Server.fs index 8401712c..90f9e717 100644 --- a/src/CSharpLanguageServer/Lsp/Server.fs +++ b/src/CSharpLanguageServer/Lsp/Server.fs @@ -53,7 +53,7 @@ type CSharpLspServer( let mutable _workspaceFolders: WorkspaceFolder list = [] let withContext - requestType + requestMode (handlerFn: ServerRequestContext -> 'a -> Async>) param = let requestName = handlerFn.ToString() @@ -63,7 +63,7 @@ type CSharpLspServer( // StreamJsonRpc lib we're using in Ionide.LanguageServerProtocol guarantees that it will not call another // handler until previous one returns a Task (in our case -- F# `async` object.) - let startRequest rc = StartRequest (requestName, requestType, 0, rc) + let startRequest rc = StartRequest (requestName, requestMode, 0, rc) let requestId, semaphore = stateActor.PostAndReply(startRequest) let stateAcquisitionAndHandlerInvocation = async { diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index 445761b1..71ed2598 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -20,7 +20,7 @@ type DecompiledMetadataDocument = { Document: Document } -type ServerRequestType = ReadOnly | ReadWrite +type ServerRequestMode = ReadOnly | ReadWrite type ServerOpenDocInfo = { @@ -31,7 +31,7 @@ type ServerOpenDocInfo = type ServerRequest = { Id: int Name: string - Type: ServerRequestType + Mode: ServerRequestMode Semaphore: SemaphoreSlim Priority: int // 0 is the highest priority, 1 is lower prio, etc. // priority is used to order pending R/O requests and is ignored wrt R/W requests @@ -80,7 +80,7 @@ let pullNextRequestMaybe requestQueue = | [] -> (None, requestQueue) | nonEmptyRequestQueue -> - let requestIsReadOnly r = r.Type = ServerRequestType.ReadOnly + let requestIsReadOnly r = (r.Mode = ReadOnly) // here we will try to take non-interrupted r/o request sequence at the front, // order it by priority and run the most prioritized one first @@ -118,7 +118,7 @@ type ServerStateEvent = | OpenDocTouch of string * DateTime | GetState of AsyncReplyChannel | GetDocumentOfTypeForUri of ServerDocumentType * string * AsyncReplyChannel - | StartRequest of string * ServerRequestType * int * AsyncReplyChannel + | StartRequest of string * ServerRequestMode * int * AsyncReplyChannel | FinishRequest of int | ProcessRequestQueue | SolutionReloadRequest of TimeSpan @@ -175,12 +175,12 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async return state - | StartRequest (name, requestType, requestPriority, replyChannel) -> + | StartRequest (name, requestMode, requestPriority, replyChannel) -> postSelf ProcessRequestQueue let newRequest = { Id=state.LastRequestId+1 Name=name - Type=requestType + Mode=requestMode Semaphore=new SemaphoreSlim(0, 1) Priority=requestPriority Enqueued=DateTime.Now } @@ -210,7 +210,7 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async let runningRWRequestMaybe = state.RunningRequests |> Seq.map (fun kv -> kv.Value) - |> Seq.tryFind (fun r -> r.Type = ReadWrite) + |> Seq.tryFind (fun r -> r.Mode = ReadWrite) // let numRunningRequests = state.RunningRequests |> Map.count From 99d0b525c6f8ecad742b6ceaba6c0a1f22eb0b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Sun, 31 Aug 2025 18:27:06 +0300 Subject: [PATCH 4/8] Use literal LSP request name for tracking request stats --- src/CSharpLanguageServer/Lsp/Server.fs | 125 ++++++++++++------------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/src/CSharpLanguageServer/Lsp/Server.fs b/src/CSharpLanguageServer/Lsp/Server.fs index 90f9e717..947390f9 100644 --- a/src/CSharpLanguageServer/Lsp/Server.fs +++ b/src/CSharpLanguageServer/Lsp/Server.fs @@ -53,11 +53,10 @@ type CSharpLspServer( let mutable _workspaceFolders: WorkspaceFolder list = [] let withContext + requestName requestMode (handlerFn: ServerRequestContext -> 'a -> Async>) param = - let requestName = handlerFn.ToString() - // we want to be careful and lock solution for change immediately w/o entering async/returing an `async` workflow // // StreamJsonRpc lib we're using in Ionide.LanguageServerProtocol guarantees that it will not call another @@ -94,8 +93,8 @@ type CSharpLspServer( |> wrapExceptionAsLspResult |> unwindProtect (fun () -> stateActor.Post(FinishRequest requestId)) - let withReadOnlyContext handlerFn = withContext ReadOnly handlerFn - let withReadWriteContext handlerFn = withContext ReadWrite handlerFn + let withReadOnlyContext requestName handlerFn = withContext requestName ReadOnly handlerFn + let withReadWriteContext requestName handlerFn = withContext requestName ReadWrite handlerFn let ignoreResult handlerFn = async { let! _ = handlerFn @@ -186,83 +185,83 @@ type CSharpLspServer( override __.Initialize(p) = let serverCapabilities = getServerCapabilities p - p |> withReadWriteContext (Initialization.handleInitialize lspClient setupTimer serverCapabilities) + p |> withReadWriteContext "initialize" (Initialization.handleInitialize lspClient setupTimer serverCapabilities) override __.Initialized(_) = - () |> withReadWriteContext (Initialization.handleInitialized lspClient stateActor getRegistrations) + () |> withReadWriteContext "initialized" (Initialization.handleInitialized lspClient stateActor getRegistrations) |> ignoreResult - override __.Shutdown() = () |> withReadWriteContext Initialization.handleShutdown + override __.Shutdown() = () |> withReadWriteContext "shutdown" Initialization.handleShutdown override __.Exit() = ignoreNotification - override __.TextDocumentHover(p) = p |> withReadOnlyContext Hover.handle - override __.TextDocumentDidOpen(p) = p |> withReadOnlyContext (TextDocumentSync.didOpen) |> ignoreResult - override __.TextDocumentDidChange(p) = p |> withReadWriteContext (TextDocumentSync.didChange) |> ignoreResult - override __.TextDocumentDidClose(p) = p |> withReadWriteContext (TextDocumentSync.didClose) |> ignoreResult - override __.TextDocumentWillSave(p) = p |> withReadWriteContext TextDocumentSync.willSave |> ignoreResult - override __.TextDocumentWillSaveWaitUntil(p) = p |> withReadWriteContext TextDocumentSync.willSaveWaitUntil - override __.TextDocumentDidSave(p) = p |> withReadWriteContext (TextDocumentSync.didSave) |> ignoreResult - override __.TextDocumentCompletion(p) = p |> withReadOnlyContext Completion.handle - override __.CompletionItemResolve(p) = p |> withReadOnlyContext Completion.resolve - override __.TextDocumentPrepareRename(p) = p |> withReadOnlyContext Rename.prepare - override __.TextDocumentRename(p) = p |> withReadOnlyContext Rename.handle - override __.TextDocumentDefinition(p) = p |> withReadOnlyContext Definition.handle - override __.TextDocumentReferences(p) = p |> withReadOnlyContext References.handle - override __.TextDocumentDocumentHighlight(p) = p |> withReadOnlyContext DocumentHighlight.handle - override __.TextDocumentDocumentLink(p) = p |> withReadOnlyContext DocumentLink.handle - override __.DocumentLinkResolve(p) = p |> withReadOnlyContext DocumentLink.resolve - override __.TextDocumentTypeDefinition(p) = p |> withReadOnlyContext TypeDefinition.handle - override __.TextDocumentImplementation(p) = p |> withReadOnlyContext Implementation.handle - override __.TextDocumentCodeAction(p) = p |> withReadOnlyContext CodeAction.handle - override __.CodeActionResolve(p) = p |> withReadOnlyContext CodeAction.resolve - override __.TextDocumentCodeLens(p) = p |> withReadOnlyContext CodeLens.handle - override __.CodeLensResolve(p) = p |> withReadOnlyContext CodeLens.resolve - override __.TextDocumentSignatureHelp(p) = p |> withReadOnlyContext SignatureHelp.handle - override __.TextDocumentDocumentColor(p) = p |> withReadOnlyContext Color.handle - override __.TextDocumentColorPresentation(p) = p |> withReadOnlyContext Color.present - override __.TextDocumentFormatting(p) = p |> withReadOnlyContext DocumentFormatting.handle - override __.TextDocumentRangeFormatting(p) = p |> withReadOnlyContext DocumentRangeFormatting.handle - override __.TextDocumentOnTypeFormatting(p) = p |> withReadOnlyContext DocumentOnTypeFormatting.handle - override __.TextDocumentDocumentSymbol(p) = p |> withReadOnlyContext DocumentSymbol.handle - override __.WorkspaceDidChangeWatchedFiles(p) = p |> withReadWriteContext Workspace.didChangeWatchedFiles |> ignoreResult + override __.TextDocumentHover(p) = p |> withReadOnlyContext "textDocument/hover" Hover.handle + override __.TextDocumentDidOpen(p) = p |> withReadOnlyContext "textDocument/didOpen" TextDocumentSync.didOpen |> ignoreResult + override __.TextDocumentDidChange(p) = p |> withReadWriteContext "textDocument/didChange" TextDocumentSync.didChange |> ignoreResult + override __.TextDocumentDidClose(p) = p |> withReadWriteContext "textDocument/didClose" TextDocumentSync.didClose |> ignoreResult + override __.TextDocumentWillSave(p) = p |> withReadWriteContext "textDocument/willSave" TextDocumentSync.willSave |> ignoreResult + override __.TextDocumentWillSaveWaitUntil(p) = p |> withReadWriteContext "textDocument/willSaveWaitUntil" TextDocumentSync.willSaveWaitUntil + override __.TextDocumentDidSave(p) = p |> withReadWriteContext "textDocument/didSave" TextDocumentSync.didSave |> ignoreResult + override __.TextDocumentCompletion(p) = p |> withReadOnlyContext "textDocument/completion" Completion.handle + override __.CompletionItemResolve(p) = p |> withReadOnlyContext "completionItem/resolve" Completion.resolve + override __.TextDocumentPrepareRename(p) = p |> withReadOnlyContext "textDocument/prepareRename" Rename.prepare + override __.TextDocumentRename(p) = p |> withReadOnlyContext "textDocument/rename" Rename.handle + override __.TextDocumentDefinition(p) = p |> withReadOnlyContext "textDocument/definition" Definition.handle + override __.TextDocumentReferences(p) = p |> withReadOnlyContext "textDocument/references" References.handle + override __.TextDocumentDocumentHighlight(p) = p |> withReadOnlyContext "textDocument/documentHighlight" DocumentHighlight.handle + override __.TextDocumentDocumentLink(p) = p |> withReadOnlyContext "textDocument/documentLink" DocumentLink.handle + override __.DocumentLinkResolve(p) = p |> withReadOnlyContext "documentLink/resolve" DocumentLink.resolve + override __.TextDocumentTypeDefinition(p) = p |> withReadOnlyContext "textDocument/typeDefinition" TypeDefinition.handle + override __.TextDocumentImplementation(p) = p |> withReadOnlyContext "textDocument/implementation" Implementation.handle + override __.TextDocumentCodeAction(p) = p |> withReadOnlyContext "textDocument/codeAction" CodeAction.handle + override __.CodeActionResolve(p) = p |> withReadOnlyContext "codeAction/resolve" CodeAction.resolve + override __.TextDocumentCodeLens(p) = p |> withReadOnlyContext "textDocument/codeLens" CodeLens.handle + override __.CodeLensResolve(p) = p |> withReadOnlyContext "codeLens/resolve" CodeLens.resolve + override __.TextDocumentSignatureHelp(p) = p |> withReadOnlyContext "textDocument/signatureHelp" SignatureHelp.handle + override __.TextDocumentDocumentColor(p) = p |> withReadOnlyContext "textDocument/documentColor" Color.handle + override __.TextDocumentColorPresentation(p) = p |> withReadOnlyContext "textDocument/colorPresentation" Color.present + override __.TextDocumentFormatting(p) = p |> withReadOnlyContext "textDocument/formatting" DocumentFormatting.handle + override __.TextDocumentRangeFormatting(p) = p |> withReadOnlyContext "textDocument/rangeFormatting" DocumentRangeFormatting.handle + override __.TextDocumentOnTypeFormatting(p) = p |> withReadOnlyContext "textDocument/onTypeFormatting" DocumentOnTypeFormatting.handle + override __.TextDocumentDocumentSymbol(p) = p |> withReadOnlyContext "textDocument/documentSymbol" DocumentSymbol.handle + override __.WorkspaceDidChangeWatchedFiles(p) = p |> withReadWriteContext "workspace/didChangeWatchedFiles" Workspace.didChangeWatchedFiles |> ignoreResult override __.WorkspaceDidChangeWorkspaceFolders(_p) = ignoreNotification - override __.WorkspaceDidChangeConfiguration(p) = p |> withReadWriteContext Workspace.didChangeConfiguration |> ignoreResult - override __.WorkspaceWillCreateFiles(_) = notImplemented + override __.WorkspaceDidChangeConfiguration(p) = p |> withReadWriteContext "workspace/didChangeConfiguration" Workspace.didChangeConfiguration |> ignoreResult + override __.WorkspaceWillCreateFiles(_) = () |> withReadOnlyContext "workspace/willCreateFiles" (fun _ _ -> notImplemented) override __.WorkspaceDidCreateFiles(_) = ignoreNotification - override __.WorkspaceWillRenameFiles(_) = notImplemented + override __.WorkspaceWillRenameFiles(_) = () |> withReadOnlyContext "workspace/willRenameFiles" (fun _ _ -> notImplemented) override __.WorkspaceDidRenameFiles(_) = ignoreNotification - override __.WorkspaceWillDeleteFiles(_) = notImplemented + override __.WorkspaceWillDeleteFiles(_) = () |> withReadOnlyContext "workspace/willDeleteFiles" (fun _ _ -> notImplemented) override __.WorkspaceDidDeleteFiles(_) = ignoreNotification - override __.WorkspaceSymbol(p) = p |> withReadOnlyContext WorkspaceSymbol.handle - override __.WorkspaceExecuteCommand(p) = p |> withReadOnlyContext ExecuteCommand.handle - override __.TextDocumentFoldingRange(p) = p |> withReadOnlyContext FoldingRange.handle - override __.TextDocumentSelectionRange(p) = p |> withReadOnlyContext SelectionRange.handle - override __.TextDocumentSemanticTokensFull(p) = p |> withReadOnlyContext SemanticTokens.handleFull - override __.TextDocumentSemanticTokensFullDelta(p) = p |> withReadOnlyContext SemanticTokens.handleFullDelta - override __.TextDocumentSemanticTokensRange(p) = p |> withReadOnlyContext SemanticTokens.handleRange - override __.TextDocumentInlayHint(p) = p |> withReadOnlyContext InlayHint.handle - override __.InlayHintResolve(p) = p |> withReadOnlyContext InlayHint.resolve + override __.WorkspaceSymbol(p) = p |> withReadOnlyContext "workspace/symbol" WorkspaceSymbol.handle + override __.WorkspaceExecuteCommand(p) = p |> withReadOnlyContext "workspace/executeCommand" ExecuteCommand.handle + override __.TextDocumentFoldingRange(p) = p |> withReadOnlyContext "textDocument/foldingRange" FoldingRange.handle + override __.TextDocumentSelectionRange(p) = p |> withReadOnlyContext "textDocument/selectionRange" SelectionRange.handle + override __.TextDocumentSemanticTokensFull(p) = p |> withReadOnlyContext "textDocument/semanticTokens/full" SemanticTokens.handleFull + override __.TextDocumentSemanticTokensFullDelta(p) = p |> withReadOnlyContext "textDocument/semanticTokens/full/delta" SemanticTokens.handleFullDelta + override __.TextDocumentSemanticTokensRange(p) = p |> withReadOnlyContext "textDocument/semanticTokens/range" SemanticTokens.handleRange + override __.TextDocumentInlayHint(p) = p |> withReadOnlyContext "textDocument/inlayHint" InlayHint.handle + override __.InlayHintResolve(p) = p |> withReadOnlyContext "inlayHint/resolve" InlayHint.resolve override __.WindowWorkDoneProgressCancel (_) = raise (System.NotImplementedException()) override __.TextDocumentInlineValue(_) = notImplemented - override __.TextDocumentPrepareCallHierarchy(p) = p |> withReadOnlyContext CallHierarchy.prepare - override __.CallHierarchyIncomingCalls(p) = p |> withReadOnlyContext CallHierarchy.incomingCalls - override __.CallHierarchyOutgoingCalls(p) = p |> withReadOnlyContext CallHierarchy.outgoingCalls - override __.TextDocumentPrepareTypeHierarchy(p) = p |> withReadOnlyContext TypeHierarchy.prepare - override __.TypeHierarchySupertypes(p) = p |> withReadOnlyContext TypeHierarchy.supertypes - override __.TypeHierarchySubtypes(p) = p |> withReadOnlyContext TypeHierarchy.subtypes - override __.TextDocumentDeclaration(p) = p |> withReadOnlyContext Declaration.handle - override __.WorkspaceDiagnostic(p) = p |> withReadOnlyContext Diagnostic.handleWorkspaceDiagnostic + override __.TextDocumentPrepareCallHierarchy(p) = p |> withReadOnlyContext "textDocument/prepareCallHierarchy" CallHierarchy.prepare + override __.CallHierarchyIncomingCalls(p) = p |> withReadOnlyContext "callHierarchy/incomingCalls" CallHierarchy.incomingCalls + override __.CallHierarchyOutgoingCalls(p) = p |> withReadOnlyContext "callHierarchy/outgoingCalls" CallHierarchy.outgoingCalls + override __.TextDocumentPrepareTypeHierarchy(p) = p |> withReadOnlyContext "textDocument/prepareTypeHierarchy" TypeHierarchy.prepare + override __.TypeHierarchySupertypes(p) = p |> withReadOnlyContext "typeHierarchy/supertypes" TypeHierarchy.supertypes + override __.TypeHierarchySubtypes(p) = p |> withReadOnlyContext "typeHierarchy/subtypes" TypeHierarchy.subtypes + override __.TextDocumentDeclaration(p) = p |> withReadOnlyContext "textDocument/declaration" Declaration.handle + override __.WorkspaceDiagnostic(p) = p |> withReadOnlyContext "workspace/diagnostic" Diagnostic.handleWorkspaceDiagnostic override __.CancelRequest(_) = ignoreNotification override __.NotebookDocumentDidChange(_) = ignoreNotification override __.NotebookDocumentDidClose(_) = ignoreNotification override __.NotebookDocumentDidOpen(_) = ignoreNotification override __.NotebookDocumentDidSave(_) = ignoreNotification - override __.WorkspaceSymbolResolve(p) = p |> withReadOnlyContext WorkspaceSymbol.resolve - override __.TextDocumentDiagnostic(p) = p |> withReadOnlyContext Diagnostic.handle - override __.TextDocumentLinkedEditingRange(p) = p |> withReadOnlyContext LinkedEditingRange.handle - override __.TextDocumentMoniker(p) = p |> withReadOnlyContext Moniker.handle + override __.WorkspaceSymbolResolve(p) = p |> withReadOnlyContext "workspaceSymbol/resolve" WorkspaceSymbol.resolve + override __.TextDocumentDiagnostic(p) = p |> withReadOnlyContext "textDocument/diagnostic" Diagnostic.handle + override __.TextDocumentLinkedEditingRange(p) = p |> withReadOnlyContext "textDocument/linkedEditingRange" LinkedEditingRange.handle + override __.TextDocumentMoniker(p) = p |> withReadOnlyContext "textDocument/moniker" Moniker.handle override __.Progress(_) = ignoreNotification override __.SetTrace(_) = ignoreNotification - override __.CSharpMetadata(p) = p |> withReadOnlyContext CSharpMetadata.handle + override __.CSharpMetadata(p) = p |> withReadOnlyContext "csharp/metadata" CSharpMetadata.handle module Server = let logger = Logging.getLoggerByName "LSP" From 58f438ddcdee19d2b6b266758df00113867842af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Sun, 31 Aug 2025 17:29:59 +0300 Subject: [PATCH 5/8] Benchmark request duration per handler --- src/CSharpLanguageServer/State/ServerState.fs | 72 +++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index 71ed2598..e95cd063 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -28,6 +28,14 @@ type ServerOpenDocInfo = Touched: DateTime } +type RequestMetrics = + { + Count: int + TotalDuration: TimeSpan + } + with + static member Zero = { Count = 0; TotalDuration = TimeSpan.Zero } + type ServerRequest = { Id: int Name: string @@ -51,6 +59,8 @@ and ServerState = { SolutionReloadPending: DateTime option PushDiagnosticsDocumentBacklog: string list PushDiagnosticsCurrentDocTask: (string * Task) option + RequestStats: Map + LastStatsDumpTime: DateTime } with static member Empty = { @@ -67,6 +77,8 @@ with SolutionReloadPending = None PushDiagnosticsDocumentBacklog = [] PushDiagnosticsCurrentDocTask = None + RequestStats = Map.empty + LastStatsDumpTime = DateTime.MinValue } @@ -126,6 +138,7 @@ type ServerStateEvent = | PushDiagnosticsProcessPendingDocuments | PushDiagnosticsDocumentDiagnosticsResolution of Result<(string * int option * Diagnostic array), Exception> | PeriodicTimerTick + | DumpAndResetRequestStats let getDocumentForUriOfType state docType (u: string) = @@ -153,6 +166,32 @@ let getDocumentForUriOfType state docType (u: string) = | AnyDocument -> matchingUserDocumentMaybe |> Option.orElse matchingDecompiledDocumentMaybe | None -> None + +let processDumpAndResetRequestStats state = + if Map.isEmpty state.RequestStats then + System.Console.Error.WriteLine("------- No request stats -------") + else + System.Console.Error.WriteLine("--------- Request Stats ---------") + let sortedStats = + state.RequestStats + |> Map.toList + |> List.map (fun (name, metrics) -> + let avgDurationMs = + if metrics.Count > 0 then metrics.TotalDuration.TotalMilliseconds / float metrics.Count + else 0.0 + (name, metrics, avgDurationMs) + ) + |> List.sortByDescending (fun (_, _, avg) -> avg) + + for (name, metrics, avgDurationMs) in sortedStats do + System.Console.Error.WriteLine($"{name}: Count={metrics.Count}, AvgDuration={avgDurationMs:F2}ms") + + System.Console.Error.WriteLine("---------------------------------") + + { state with RequestStats = Map.empty + LastStatsDumpTime = DateTime.Now } + + let processServerEvent (logger: ILogger) state postSelf msg : Async = async { match msg with | SettingsChange newSettings -> @@ -193,10 +232,25 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async | FinishRequest requestId -> let request = state.RunningRequests |> Map.tryFind requestId match request with - | Some(request) -> + | Some request -> request.Semaphore.Dispose() + + let newRequestStats = + let requestDuration = DateTime.Now - request.Enqueued + + let updateRequestStats stats = + match stats with + | Some s -> Some { Count = s.Count + 1; TotalDuration = s.TotalDuration + requestDuration } + | None -> Some { Count = 1; TotalDuration = requestDuration } + + match state.Settings.DebugMode with + | true -> state.RequestStats |> Map.change request.Name updateRequestStats + | false -> state.RequestStats + let newRunningRequests = state.RunningRequests |> Map.remove requestId - let newState = { state with RunningRequests = newRunningRequests } + + let newState = { state with RunningRequests = newRunningRequests + RequestStats = newRequestStats } postSelf ProcessRequestQueue return newState @@ -396,10 +450,15 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async | PeriodicTimerTick -> postSelf PushDiagnosticsProcessPendingDocuments - let solutionReloadTime = state.SolutionReloadPending - |> Option.defaultValue (DateTime.Now.AddDays(1)) + let statsDumpDeadline = state.LastStatsDumpTime + TimeSpan.FromMinutes(1.0) + if state.Settings.DebugMode && statsDumpDeadline < DateTime.Now then + postSelf DumpAndResetRequestStats - match solutionReloadTime < DateTime.Now with + let solutionReloadDeadline = + state.SolutionReloadPending + |> Option.defaultValue (DateTime.Now.AddDays(1)) + + match solutionReloadDeadline < DateTime.Now with | true -> let! newSolution = loadSolutionOnSolutionPathOrDir @@ -413,6 +472,9 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async | false -> return state + + | DumpAndResetRequestStats -> + return processDumpAndResetRequestStats state } let serverEventLoop initialState (inbox: MailboxProcessor) = From 3384c11d6bfec811e60a85cd15dc4182e4355a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Sun, 31 Aug 2025 19:23:48 +0300 Subject: [PATCH 6/8] Format stats in column --- src/CSharpLanguageServer/State/ServerState.fs | 54 +++++++++++++------ src/CSharpLanguageServer/Util.fs | 29 ++++++++++ 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/CSharpLanguageServer/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index e95cd063..3387908c 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -14,6 +14,7 @@ open CSharpLanguageServer.RoslynHelpers open CSharpLanguageServer.Types open CSharpLanguageServer.Logging open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Util type DecompiledMetadataDocument = { Metadata: CSharpMetadataInformation @@ -32,9 +33,12 @@ type RequestMetrics = { Count: int TotalDuration: TimeSpan + MaxDuration: TimeSpan } with - static member Zero = { Count = 0; TotalDuration = TimeSpan.Zero } + static member Zero = { Count = 0 + TotalDuration = TimeSpan.Zero + MaxDuration = TimeSpan.Zero } type ServerRequest = { Id: int @@ -168,25 +172,37 @@ let getDocumentForUriOfType state docType (u: string) = let processDumpAndResetRequestStats state = - if Map.isEmpty state.RequestStats then - System.Console.Error.WriteLine("------- No request stats -------") - else - System.Console.Error.WriteLine("--------- Request Stats ---------") + let formatStats stats = + let calculateRequestStatsMetrics (name, metrics) = + let avgDurationMs = + if metrics.Count > 0 then metrics.TotalDuration.TotalMilliseconds / float metrics.Count + else 0.0 + (name, metrics, avgDurationMs) + let sortedStats = - state.RequestStats + stats |> Map.toList - |> List.map (fun (name, metrics) -> - let avgDurationMs = - if metrics.Count > 0 then metrics.TotalDuration.TotalMilliseconds / float metrics.Count - else 0.0 - (name, metrics, avgDurationMs) - ) + |> List.map calculateRequestStatsMetrics |> List.sortByDescending (fun (_, _, avg) -> avg) - for (name, metrics, avgDurationMs) in sortedStats do - System.Console.Error.WriteLine($"{name}: Count={metrics.Count}, AvgDuration={avgDurationMs:F2}ms") + let formatStatsRow (name, metrics, avgDurationMs: float) = + [ $"\"{name}\"" + metrics.Count |> string + avgDurationMs.ToString("F2") + metrics.MaxDuration.TotalMilliseconds.ToString("F2") ] + + let headerRow = ["Name"; "Count"; "AvgDuration (ms)"; "MaxDuration (ms)"] + let dataRows = sortedStats |> List.map formatStatsRow + + formatInColumns (headerRow :: dataRows) + + if not (Map.isEmpty state.RequestStats) then + System.Console.Error.WriteLine("--------- Request Stats ---------") + System.Console.Error.WriteLine(state.RequestStats |> formatStats) System.Console.Error.WriteLine("---------------------------------") + else + System.Console.Error.WriteLine("------- No request stats -------") { state with RequestStats = Map.empty LastStatsDumpTime = DateTime.Now } @@ -240,8 +256,14 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async let updateRequestStats stats = match stats with - | Some s -> Some { Count = s.Count + 1; TotalDuration = s.TotalDuration + requestDuration } - | None -> Some { Count = 1; TotalDuration = requestDuration } + | Some s -> + Some { Count = s.Count + 1 + TotalDuration = s.TotalDuration + requestDuration + MaxDuration = max s.MaxDuration requestDuration } + | None -> + Some { Count = 1 + TotalDuration = requestDuration + MaxDuration = requestDuration } match state.Settings.DebugMode with | true -> state.RequestStats |> Map.change request.Name updateRequestStats diff --git a/src/CSharpLanguageServer/Util.fs b/src/CSharpLanguageServer/Util.fs index 990f5b4a..88262e23 100644 --- a/src/CSharpLanguageServer/Util.fs +++ b/src/CSharpLanguageServer/Util.fs @@ -52,6 +52,35 @@ let curry f x y = f (x, y) let uncurry f (x, y) = f x y +let formatInColumns (data: list>) : string = + if List.isEmpty data then + "" + else + let numCols = data |> List.map List.length |> List.max + + let columnWidths = + [0 .. numCols - 1] + |> List.map (fun colIdx -> + data + |> List.map (fun row -> + if colIdx < row.Length then row.[colIdx].Length + else 0 + ) + |> List.max) + + data + |> List.map (fun row -> + [0 .. numCols - 1] + |> List.map (fun colIdx -> + let value = if colIdx < row.Length then row.[colIdx] else "" + let width = columnWidths.[colIdx] + value.PadRight(width) + ) + |> String.concat " " + ) + |> String.concat System.Environment.NewLine + + module Seq = let inline tryMaxBy (projection: 'T -> 'U) (source: 'T seq): 'T option = if isNull source || Seq.isEmpty source then From 1ac070f65310cb7334f75e9973eb4ce12a4da081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Sun, 31 Aug 2025 19:26:43 +0300 Subject: [PATCH 7/8] Dump server settings on startup --- src/CSharpLanguageServer/Handlers/Initialization.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CSharpLanguageServer/Handlers/Initialization.fs b/src/CSharpLanguageServer/Handlers/Initialization.fs index 07b983d8..427441c6 100644 --- a/src/CSharpLanguageServer/Handlers/Initialization.fs +++ b/src/CSharpLanguageServer/Handlers/Initialization.fs @@ -34,6 +34,7 @@ module Initialization = let serverName = "csharp-ls" let serverVersion = Assembly.GetExecutingAssembly().GetName().Version |> string logger.LogInformation("initializing, {name} version {version}", serverName, serverVersion) + logger.LogInformation("server settings: {settings}", context.State.Settings |> string) do! windowShowMessage( sprintf "csharp-ls: initializing, version %s" serverVersion) From 8b7af307a5adf72eaf0d4ff78af50b8b348d2705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Saulius=20Menkevi=C4=8Dius?= Date: Sun, 31 Aug 2025 18:54:13 +0300 Subject: [PATCH 8/8] Add CHANGELOG.md entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2315a0a7..7078f5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [Unreleased] +* Print request stats in debug mode. + - By @razzmatazz in https://github.com/razzmatazz/csharp-language-server/pull/258 + ## [0.19.0] - 2025-08-20 / KapĨiamiestis * Select common target framework when multiple projects are found - By @AdeAttwood in https://github.com/razzmatazz/csharp-language-server/pull/253