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 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) diff --git a/src/CSharpLanguageServer/Lsp/Server.fs b/src/CSharpLanguageServer/Lsp/Server.fs index 1b1a1462..947390f9 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)) @@ -53,17 +53,16 @@ type CSharpLspServer( let mutable _workspaceFolders: WorkspaceFolder list = [] let withContext - requestType + 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 // 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 { @@ -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" 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/State/ServerState.fs b/src/CSharpLanguageServer/State/ServerState.fs index cbb2f0ac..3387908c 100644 --- a/src/CSharpLanguageServer/State/ServerState.fs +++ b/src/CSharpLanguageServer/State/ServerState.fs @@ -14,13 +14,14 @@ open CSharpLanguageServer.RoslynHelpers open CSharpLanguageServer.Types open CSharpLanguageServer.Logging open CSharpLanguageServer.Conversions +open CSharpLanguageServer.Util type DecompiledMetadataDocument = { Metadata: CSharpMetadataInformation Document: Document } -type ServerRequestType = ReadOnly | ReadWrite +type ServerRequestMode = ReadOnly | ReadWrite type ServerOpenDocInfo = { @@ -28,10 +29,21 @@ type ServerOpenDocInfo = Touched: DateTime } +type RequestMetrics = + { + Count: int + TotalDuration: TimeSpan + MaxDuration: TimeSpan + } + with + static member Zero = { Count = 0 + TotalDuration = TimeSpan.Zero + MaxDuration = TimeSpan.Zero } + 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 @@ -51,7 +63,28 @@ and ServerState = { SolutionReloadPending: DateTime option PushDiagnosticsDocumentBacklog: string list PushDiagnosticsCurrentDocTask: (string * Task) option + RequestStats: Map + LastStatsDumpTime: DateTime } +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 + RequestStats = Map.empty + LastStatsDumpTime = DateTime.MinValue + } + let pullFirstRequestMaybe requestQueue = match requestQueue with @@ -63,7 +96,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 @@ -83,21 +116,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 @@ -116,7 +134,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 @@ -124,6 +142,7 @@ type ServerStateEvent = | PushDiagnosticsProcessPendingDocuments | PushDiagnosticsDocumentDiagnosticsResolution of Result<(string * int option * Diagnostic array), Exception> | PeriodicTimerTick + | DumpAndResetRequestStats let getDocumentForUriOfType state docType (u: string) = @@ -151,6 +170,44 @@ let getDocumentForUriOfType state docType (u: string) = | AnyDocument -> matchingUserDocumentMaybe |> Option.orElse matchingDecompiledDocumentMaybe | None -> None + +let processDumpAndResetRequestStats state = + 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 = + stats + |> Map.toList + |> List.map calculateRequestStatsMetrics + |> List.sortByDescending (fun (_, _, avg) -> avg) + + 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 } + + let processServerEvent (logger: ILogger) state postSelf msg : Async = async { match msg with | SettingsChange newSettings -> @@ -173,12 +230,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 } @@ -191,10 +248,31 @@ 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 + 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 + | 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 @@ -208,7 +286,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 @@ -394,10 +472,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 @@ -411,6 +494,9 @@ let processServerEvent (logger: ILogger) state postSelf msg : Async | false -> return state + + | DumpAndResetRequestStats -> + return processDumpAndResetRequestStats state } let serverEventLoop initialState (inbox: MailboxProcessor) = 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 = 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