Repository: sublimelsp/LSP

Issue:
Use timestamps on the logging messages
On:

1. https://github.com/palantir/python-language-server/issues/257 Took 2 minutes to respond to on_hover(), from 15:56:14 until 15:58:38

I had reported the LSP log in completion to the `python-language-server` log. Howerver LSP logs have no timestamps, then make impossible to correlated between the two logs.

I wrote a extension to the built-in logging module https://github.com/evandrocoan/PythonDebugTools and I am going to add it on my fork of LSP. Then I will open pull request and you can see if you like it.


Discussion:
Agree that timing is useful to have in LSP!
For performance investigations, developers can make the necessary change to 'logging.py'.

Closing until more endusers report a preference for more logging output.
Something like this?
```
:: --> LSP-pylsp initialize (1 | 12:18:01): {'initializationOptions': {}, 'capabilities': {'general': {'markdown': {'version': '3.2.2', 'parser': 'Python-Markdown'}, 'regularExpressions': {'engine': 'ECMAScript'}}, 'window': {'showDocument': {'support': True}, 'showMessage': {'messageActionItem': {'additionalPropertiesSupport': True}}, 'workDoneProgress': True}, 'workspace': {'configuration': True, 'symbol': {'tagSupport': {'valueSet': [1]}, 'symbolKind': {'valueSet': [15, 7, 2, 5, 18, 19, 25, 23, 1, 14, 21, 9, 4, 11, 6, 8, 22, 16, 3, 20, 26, 13, 17, 24, 12, 10]}, 'dynamicRegistration': True}, 'executeCommand': {}, 'applyEdit': True, 'semanticTokens': {'refreshSupport': True}, 'inlayHint': {'refreshSupport': True}, 'workspaceFolders': True, 'didChangeWatchedFiles': {'dynamicRegistration': True}, 'workspaceEdit': {'failureHandling': 'abort', 'documentChanges': True}, 'didChangeConfiguration': {'dynamicRegistration': True}, 'codeLens': {'refreshSupport': True}}, 'textDocument': {'completion': {'completionItem': {'tagSupport': {'valueSet': [1]}, 'labelDetailsSupport': True, 'insertReplaceSupport': True, 'documentationFormat': ['markdown', 'plaintext'], 'deprecatedSupport': True, 'insertTextModeSupport': {'valueSet': [2]}, 'resolveSupport': {'properties': ['detail', 'documentation', 'additionalTextEdits']}, 'snippetSupport': True}, 'insertTextMode': 2, 'dynamicRegistration': True, 'completionItemKind': {'valueSet': [12, 10, 9, 7, 24, 22, 17, 21, 14, 11, 4, 16, 15, 8, 5, 20, 18, 2, 25, 19, 6, 23, 3, 1, 13]}}, 'rename': {'prepareSupport': True, 'dynamicRegistration': True}, 'documentLink': {'tooltipSupport': True, 'dynamicRegistration': True}, 'signatureHelp': {'contextSupport': True, 'signatureInformation': {'documentationFormat': ['markdown', 'plaintext'], 'activeParameterSupport': True, 'parameterInformation': {'labelOffsetSupport': True}}, 'dynamicRegistration': True}, 'inlayHint': {'resolveSupport': {'properties': ['textEdits', 'label.command']}, 'dynamicRegistration': True}, 'codeAction': {'disabledSupport': True, 'resolveSupport': {'properties': ['edit']}, 'dynamicRegistration': True, 'isPreferredSupport': True, 'codeActionLiteralSupport': {'codeActionKind': {'valueSet': ['quickfix', 'refactor', 'refactor.extract', 'refactor.inline', 'refactor.rewrite', 'source.organizeImports']}}, 'dataSupport': True}, 'formatting': {'dynamicRegistration': True}, 'hover': {'dynamicRegistration': True, 'contentFormat': ['markdown', 'plaintext']}, 'references': {'dynamicRegistration': True}, 'semanticTokens': {'overlappingTokenSupport': False, 'tokenModifiers': ['static', 'abstract', 'async', 'declaration', 'modification', 'deprecated', 'definition', 'defaultLibrary', 'readonly', 'documentation'], 'requests': {'range': True, 'full': {'delta': True}}, 'tokenTypes': ['string', 'property', 'keyword', 'class', 'type', 'operator', 'struct', 'typeParameter', 'regexp', 'parameter', 'macro', 'comment', 'interface', 'method', 'enumMember', 'number', 'namespace', 'modifier', 'decorator', 'variable', 'event', 'function', 'enum'], 'dynamicRegistration': True, 'augmentsSyntaxTokens': True, 'multilineTokenSupport': True, 'formats': ['relative']}, 'selectionRange': {'dynamicRegistration': True}, 'implementation': {'linkSupport': True, 'dynamicRegistration': True}, 'synchronization': {'willSaveWaitUntil': True, 'willSave': True, 'dynamicRegistration': True, 'didSave': True}, 'declaration': {'linkSupport': True, 'dynamicRegistration': True}, 'rangeFormatting': {'dynamicRegistration': True}, 'documentSymbol': {'hierarchicalDocumentSymbolSupport': True, 'tagSupport': {'valueSet': [1]}, 'symbolKind': {'valueSet': [15, 7, 2, 5, 18, 19, 25, 23, 1, 14, 21, 9, 4, 11, 6, 8, 22, 16, 3, 20, 26, 13, 17, 24, 12, 10]}, 'dynamicRegistration': True}, 'definition': {'linkSupport': True, 'dynamicRegistration': True}, 'typeDefinition': {'linkSupport': True, 'dynamicRegistration': True}, 'publishDiagnostics': {'versionSupport': True, 'codeDescriptionSupport': True, 'tagSupport': {'valueSet': [2, 1]}, 'dataSupport': True, 'relatedInformation': True}, 'documentHighlight': {'dynamicRegistration': True}, 'colorProvider': {'dynamicRegistration': True}, 'codeLens': {'dynamicRegistration': True}}}, 'rootPath': '/home/predragnikolic/.config/sublime-text/Packages/LSP', 'clientInfo': {'name': 'Sublime Text LSP', 'version': '1.18.0'}, 'workspaceFolders': [{'name': 'LSP', 'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP'}], 'processId': 183133, 'rootUri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP'}
:: <<< LSP-pylsp (1 | 12:18:02 - 1687ms): {'capabilities': {'renameProvider': True, 'completionProvider': {'resolveProvider': True, 'triggerCharacters': ['.']}, 'codeLensProvider': {'resolveProvider': False}, 'documentRangeFormattingProvider': True, 'executeCommandProvider': {'commands': []}, 'documentSymbolProvider': True, 'signatureHelpProvider': {'triggerCharacters': ['(', ',', '=']}, 'definitionProvider': True, 'codeActionProvider': True, 'documentHighlightProvider': True, 'textDocumentSync': {'didClose': {}, 'didOpen': {}, 'save': {'includeText': True}, 'change': {'syncKind': 2}}, 'experimental': {}, 'workspace': {'workspaceFolders': {'supported': True, 'changeNotifications': True}}, 'documentFormattingProvider': True, 'foldingRangeProvider': True, 'hoverProvider': True, 'referencesProvider': True}, 'serverInfo': {'name': 'pylsp', 'version': '1.5.0'}}
::  -> LSP-pylsp initialized (12:18:02): {}
::  -> LSP-pylsp workspace/didChangeConfiguration (12:18:02): {'settings': {'pylsp': {'rope': {'extensionModules': None, 'ropeFolder': None}, 'plugins': {'yapf': {'enabled': False}, 'rope_completion': {'enabled': False, 'eager': False}, 'jedi_completion': {'resolve_at_most': 25, 'cache_for': ['pandas', 'numpy', 'tensorflow', 'matplotlib'], 'include_params': True, 'eager': False, 'enabled': True, 'fuzzy': True, 'include_class_objects': True}, 'pylsp_black': {'enabled': False, 'cache_config': False, 'line_length': 88, 'preview': False}, 'pylsp_mypy': {'dmypy': False, 'strict': False, 'enabled': False, 'live_mode': True}, 'pylint': {'enabled': False, 'args': None, 'executable': ''}, 'jedi_definition': {'enabled': True, 'follow_imports': True, 'follow_builtin_imports': True}, 'mccabe': {'enabled': True, 'threshold': 15}, 'pydocstyle': {'ignore': None, 'select': None, 'addIgnore': None, 'addSelect': None, 'matchDir': '[^\\.].*', 'enabled': False, 'convention': None, 'match': '(?!test_).*\\.py'}, 'flake8': {'ignore': None, 'select': None, 'perFileIgnores': None, 'filename': None, 'config': None, 'enabled': False, 'maxLineLength': None, 'hangClosing': None, 'executable': 'flake8', 'exclude': None}, 'autopep8': {'enabled': True}, 'jedi_signature_help': {'enabled': True}, 'jedi': {'environment': None, 'extra_paths': ['/opt/sublime_text/Lib/python33', '/home/predragnikolic/.config/sublime-text/Packages'], 'env_vars': None}, 'jedi_hover': {'enabled': True}, 'pyls_isort': {'enabled': False}, 'jedi_symbols': {'enabled': True, 'include_import_symbols': True, 'all_scopes': True}, 'pyflakes': {'enabled': False}, 'pycodestyle': {'ignore': None, 'select': None, 'filename': None, 'aggressive': None, 'maxLineLength': None, 'hangClosing': None, 'enabled': True, 'exclude': None}, 'jedi_references': {'enabled': True}, 'preload': {'enabled': True, 'modules': None}}, 'configurationSources': ['pycodestyle']}}}
::  -> LSP-pylsp textDocument/didOpen (12:18:03): {'textDocument': {'text': 'from ...third_party import WebsocketServer  # type: ignore\nfrom .configurations import ConfigManager\nfrom .configurations import WindowConfigManager\nfrom .diagnostics import ensure_diagnostics_panel\nfrom .diagnostics_storage import is_severity_included\nfrom .logging import debug\nfrom .logging import exception_log\nfrom .message_request_handler import MessageRequestHandler\nfrom .panels import ensure_log_panel\nfrom .panels import is_panel_open\nfrom .panels import log_server_message\nfrom .panels import PanelName\nfrom .promise import Promise\nfrom .protocol import DocumentUri\nfrom .protocol import Error\nfrom .protocol import Location\nfrom .protocol import LocationLink\nfrom .sessions import AbstractViewListener\nfrom .sessions import get_plugin\nfrom .sessions import Logger\nfrom .sessions import Manager\nfrom .sessions import Session\nfrom .settings import userprefs\nfrom .transports import create_transport\nfrom .types import ClientConfig\nfrom .types import matches_pattern\nfrom .typing import Optional, Any, Dict, Deque, List, Generator, Tuple, Union\nfrom .url import parse_uri\nfrom .views import extract_variables\nfrom .views import format_diagnostic_for_panel\nfrom .views import make_link\nfrom .workspace import ProjectFolders\nfrom .workspace import sorted_workspace_folders\nfrom collections import deque\nfrom collections import OrderedDict\nfrom subprocess import CalledProcessError\nfrom time import time, strftime\nfrom weakref import ref\nfrom weakref import WeakSet\nimport functools\nimport json\nimport sublime\nimport threading\nimport urllib.parse\n\n\n_NO_DIAGNOSTICS_PLACEHOLDER = "  No diagnostics. Well done!"\n\n\ndef extract_message(params: Any) -> str:\n    return params.get("message", "???") if isinstance(params, dict) else "???"\n\n\ndef set_diagnostics_count(view: sublime.View, errors: int, warnings: int) -> None:\n    try:\n        key = AbstractViewListener.TOTAL_ERRORS_AND_WARNINGS_STATUS_KEY\n        if userprefs().show_diagnostics_count_in_view_status:\n            view.set_status(key, "E: {}, W: {}".format(errors, warnings))\n        else:\n            view.erase_status(key)\n    except Exception:\n        pass\n\n\nclass WindowManager(Manager):\n\n    DIAGNOSTIC_PHANTOM_KEY = "lsp_diagnostic_phantom"\n\n    def __init__(\n        self,\n        window: sublime.Window,\n        workspace: ProjectFolders,\n        configs: WindowConfigManager,\n    ) -> None:\n        self._window = window\n        self._configs = configs\n        self._sessions = WeakSet()  # type: WeakSet[Session]\n        self._workspace = workspace\n        self._pending_listeners = deque()  # type: Deque[AbstractViewListener]\n        self._listeners = WeakSet()  # type: WeakSet[AbstractViewListener]\n        self._new_listener = None  # type: Optional[AbstractViewListener]\n        self._new_session = None  # type: Optional[Session]\n        self._panel_code_phantoms = None  # type: Optional[sublime.PhantomSet]\n        self.total_error_count = 0\n        self.total_warning_count = 0\n        sublime.set_timeout(functools.partial(self._update_panel_main_thread, _NO_DIAGNOSTICS_PLACEHOLDER, []))\n        ensure_log_panel(window)\n\n    def get_config_manager(self) -> WindowConfigManager:\n        return self._configs\n\n    def get_sessions(self) -> Generator[Session, None, None]:\n        yield from self._sessions\n\n    def on_load_project_async(self) -> None:\n        self.update_workspace_folders_async()\n        self._configs.update()\n\n    def on_post_save_project_async(self) -> None:\n        self.on_load_project_async()\n\n    def update_workspace_folders_async(self) -> None:\n        if self._workspace.update():\n            workspace_folders = self._workspace.get_workspace_folders()\n            for session in self._sessions:\n                session.update_folders(workspace_folders)\n\n    def enable_config_async(self, config_name: str) -> None:\n        self._configs.enable_config(config_name)\n\n    def disable_config_async(self, config_name: str) -> None:\n        self._configs.disable_config(config_name)\n\n    def open_location_async(\n        self,\n        location: Union[Location, LocationLink],\n        session_name: Optional[str],\n        view: sublime.View,\n        flags: int = 0,\n        group: int = -1\n    ) -> Promise[Optional[sublime.View]]:\n        for session in self.sessions(view):\n            if session_name is None or session_name == session.config.name:\n                return session.open_location_async(location, flags, group)\n        return Promise.resolve(None)\n\n    def register_listener_async(self, listener: AbstractViewListener) -> None:\n        set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count)\n        # Update workspace folders in case the user have changed those since window was created.\n        # There is no currently no notification in ST that would notify about folder changes.\n        self.update_workspace_folders_async()\n        self._pending_listeners.appendleft(listener)\n        if self._new_listener is None:\n            self._dequeue_listener_async()\n\n    def unregister_listener_async(self, listener: AbstractViewListener) -> None:\n        self._listeners.discard(listener)\n\n    def listeners(self) -> Generator[AbstractViewListener, None, None]:\n        yield from self._listeners\n\n    def listener_for_view(self, view: sublime.View) -> Optional[AbstractViewListener]:\n        for listener in self.listeners():\n            if listener.view == view:\n                return listener\n        return None\n\n    def _dequeue_listener_async(self) -> None:\n        listener = None  # type: Optional[AbstractViewListener]\n        if self._new_listener is not None:\n            listener = self._new_listener\n            # debug("re-checking listener", listener)\n            self._new_listener = None\n        else:\n            try:\n                listener = self._pending_listeners.pop()\n                if not listener.view.is_valid():\n                    # debug("listener", listener, "is no longer valid")\n                    return self._dequeue_listener_async()\n                # debug("adding new pending listener", listener)\n                self._listeners.add(listener)\n            except IndexError:\n                # We have handled all pending listeners.\n                self._new_session = None\n                return\n        if self._new_session:\n            self._sessions.add(self._new_session)\n        self._publish_sessions_to_listener_async(listener)\n        if self._new_session:\n            if not any(self._new_session.session_views_async()):\n                self._sessions.discard(self._new_session)\n                self._new_session.end_async()\n            self._new_session = None\n        config = self._needed_config(listener.view)\n        if config:\n            # debug("found new config for listener", listener)\n            self._new_listener = listener\n            self.start_async(config, listener.view)\n        else:\n            # debug("no new config found for listener", listener)\n            self._new_listener = None\n            self._dequeue_listener_async()\n\n    def _publish_sessions_to_listener_async(self, listener: AbstractViewListener) -> None:\n        inside_workspace = self._workspace.contains(listener.view)\n        scheme = urllib.parse.urlparse(listener.get_uri()).scheme\n        for session in self._sessions:\n            if session.can_handle(listener.view, scheme, capability=None, inside_workspace=inside_workspace):\n                # debug("registering session", session.config.name, "to listener", listener)\n                try:\n                    listener.on_session_initialized_async(session)\n                except Exception as ex:\n                    message = "failed to register session {} to listener {}".format(session.config.name, listener)\n                    exception_log(message, ex)\n\n    def window(self) -> sublime.Window:\n        return self._window\n\n    def sessions(self, view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]:\n        inside_workspace = self._workspace.contains(view)\n        sessions = list(self._sessions)\n        uri = view.settings().get("lsp_uri")\n        if not isinstance(uri, str):\n            return\n        scheme = urllib.parse.urlparse(uri).scheme\n        for session in sessions:\n            if session.can_handle(view, scheme, capability, inside_workspace):\n                yield session\n\n    def get_session(self, config_name: str, file_path: str) -> Optional[Session]:\n        return self._find_session(config_name, file_path)\n\n    def _can_start_config(self, config_name: str, file_path: str) -> bool:\n        return not bool(self._find_session(config_name, file_path))\n\n    def _find_session(self, config_name: str, file_path: str) -> Optional[Session]:\n        inside = self._workspace.contains(file_path)\n        for session in self._sessions:\n            if session.config.name == config_name and session.handles_path(file_path, inside):\n                return session\n        return None\n\n    def _needed_config(self, view: sublime.View) -> Optional[ClientConfig]:\n        configs = self._configs.match_view(view)\n        handled = False\n        file_name = view.file_name()\n        inside = self._workspace.contains(view)\n        for config in configs:\n            handled = False\n            for session in self._sessions:\n                if config.name == session.config.name and session.handles_path(file_name, inside):\n                    handled = True\n                    break\n            if not handled:\n                return config\n        return None\n\n    def start_async(self, config: ClientConfig, initiating_view: sublime.View) -> None:\n        config = ClientConfig.from_config(config, {})\n        file_path = initiating_view.file_name() or \'\'\n        if not self._can_start_config(config.name, file_path):\n            # debug(\'Already starting on this window:\', config.name)\n            return\n        try:\n            workspace_folders = sorted_workspace_folders(self._workspace.folders, file_path)\n            plugin_class = get_plugin(config.name)\n            variables = extract_variables(self._window)\n            cwd = None  # type: Optional[str]\n            if plugin_class is not None:\n                if plugin_class.needs_update_or_installation():\n                    config.set_view_status(initiating_view, "installing...")\n                    plugin_class.install_or_update()\n                additional_variables = plugin_class.additional_variables()\n                if isinstance(additional_variables, dict):\n                    variables.update(additional_variables)\n                cannot_start_reason = plugin_class.can_start(self._window, initiating_view, workspace_folders, config)\n                if cannot_start_reason:\n                    config.erase_view_status(initiating_view)\n                    message = "cannot start {}: {}".format(config.name, cannot_start_reason)\n                    self._configs.disable_config(config.name, only_for_session=True)\n                    # Continue with handling pending listeners\n                    self._new_session = None\n                    sublime.set_timeout_async(self._dequeue_listener_async)\n                    return self._window.status_message(message)\n                cwd = plugin_class.on_pre_start(self._window, initiating_view, workspace_folders, config)\n            config.set_view_status(initiating_view, "starting...")\n            session = Session(self, self._create_logger(config.name), workspace_folders, config, plugin_class)\n            if cwd:\n                transport_cwd = cwd  # type: Optional[str]\n            else:\n                transport_cwd = workspace_folders[0].path if workspace_folders else None\n            transport_config = config.resolve_transport_config(variables)\n            transport = create_transport(transport_config, transport_cwd, session)\n            if plugin_class:\n                plugin_class.on_post_start(self._window, initiating_view, workspace_folders, config)\n            config.set_view_status(initiating_view, "initialize")\n            session.initialize_async(\n                variables=variables,\n                transport=transport,\n                working_directory=cwd,\n                init_callback=functools.partial(self._on_post_session_initialize, initiating_view)\n            )\n            self._new_session = session\n        except Exception as e:\n            message = "".join((\n                "Failed to start {0} - disabling for this window for the duration of the current session.\\n",\n                "Re-enable by running \\"LSP: Enable Language Server In Project\\" from the Command Palette.",\n                "\\n\\n--- Error: ---\\n{1}"\n            )).format(config.name, str(e))\n            exception_log("Unable to start subprocess for {}".format(config.name), e)\n            if isinstance(e, CalledProcessError):\n                print("Server output:\\n{}".format(e.output.decode(\'utf-8\', \'replace\')))\n            self._configs.disable_config(config.name, only_for_session=True)\n            config.erase_view_status(initiating_view)\n            sublime.message_dialog(message)\n            # Continue with handling pending listeners\n            self._new_session = None\n            sublime.set_timeout_async(self._dequeue_listener_async)\n\n    def _on_post_session_initialize(\n        self, initiating_view: sublime.View, session: Session, is_error: bool = False\n    ) -> None:\n        if is_error:\n            session.config.erase_view_status(initiating_view)\n            self._new_listener = None\n            self._new_session = None\n        else:\n            sublime.set_timeout_async(self._dequeue_listener_async)\n\n    def _create_logger(self, config_name: str) -> Logger:\n        logger_map = {\n            "panel": PanelLogger,\n            "remote": RemoteLogger,\n        }\n        loggers = []\n        for logger_type in userprefs().log_server:\n            if logger_type not in logger_map:\n                debug("Invalid logger type ({}) specified for log_server settings".format(logger_type))\n                continue\n            loggers.append(logger_map[logger_type])\n        if len(loggers) == 0:\n            return RouterLogger()  # logs nothing\n        elif len(loggers) == 1:\n            return loggers[0](self, config_name)\n        else:\n            router_logger = RouterLogger()\n            for logger in loggers:\n                router_logger.append(logger(self, config_name))\n            return router_logger\n\n    def handle_message_request(self, session: Session, params: Any, request_id: Any) -> None:\n        view = self._window.active_view()\n        if view:\n            MessageRequestHandler(view, session, request_id, params, session.config.name).show()\n\n    def restart_sessions_async(self, config_name: Optional[str] = None) -> None:\n        self._end_sessions_async(config_name)\n        listeners = list(self._listeners)\n        self._listeners.clear()\n        for listener in listeners:\n            self.register_listener_async(listener)\n\n    def _end_sessions_async(self, config_name: Optional[str] = None) -> None:\n        sessions = list(self._sessions)\n        for session in sessions:\n            if config_name is None or config_name == session.config.name:\n                session.end_async()\n                self._sessions.discard(session)\n\n    def get_project_path(self, file_path: str) -> Optional[str]:\n        candidate = None  # type: Optional[str]\n        for folder in self._workspace.folders:\n            if file_path.startswith(folder):\n                if candidate is None or len(folder) > len(candidate):\n                    candidate = folder\n        return candidate\n\n    def should_present_diagnostics(self, uri: DocumentUri) -> Optional[str]:\n        scheme, path = parse_uri(uri)\n        if scheme != "file":\n            return None\n        if not self._workspace.contains(path):\n            return "not inside window folders"\n        view = self._window.active_view()\n        if not view:\n            return None\n        settings = view.settings()\n        if matches_pattern(path, settings.get("binary_file_patterns")):\n            return "matches a pattern in binary_file_patterns"\n        if matches_pattern(path, settings.get("file_exclude_patterns")):\n            return "matches a pattern in file_exclude_patterns"\n        if matches_pattern(path, settings.get("folder_exclude_patterns")):\n            return "matches a pattern in folder_exclude_patterns"\n        return None\n\n    def on_post_exit_async(self, session: Session, exit_code: int, exception: Optional[Exception]) -> None:\n        self._sessions.discard(session)\n        for listener in self._listeners:\n            listener.on_session_shutdown_async(session)\n        if exit_code != 0 or exception:\n            config = session.config\n            msg = "".join((\n                "{0} exited with status code {1}. ",\n                "Do you want to restart it? If you choose Cancel, it will be disabled for this window for the ",\n                "duration of the current session. ",\n                "Re-enable by running \\"LSP: Enable Language Server In Project\\" from the Command Palette."\n            )).format(config.name, exit_code)\n            if exception:\n                msg += "\\n\\n--- Error: ---\\n{}".format(str(exception))\n            if sublime.ok_cancel_dialog(msg, "Restart {}".format(config.name)):\n                for listener in self._listeners:\n                    self.register_listener_async(listener)\n            else:\n                self._configs.disable_config(config.name, only_for_session=True)\n\n    def plugin_unloaded(self) -> None:\n        """\n        This is called **from the main thread** when the plugin unloads. In that case we must destroy all sessions\n        from the main thread. That could lead to some dict/list being mutated while iterated over, so be careful\n        """\n        self._end_sessions_async()\n\n    def handle_server_message(self, server_name: str, message: str) -> None:\n        sublime.set_timeout(lambda: log_server_message(self._window, server_name, message))\n\n    def handle_log_message(self, session: Session, params: Any) -> None:\n        self.handle_server_message(session.config.name, extract_message(params))\n\n    def handle_stderr_log(self, session: Session, message: str) -> None:\n        self.handle_server_message(session.config.name, message)\n\n    def handle_show_message(self, session: Session, params: Any) -> None:\n        sublime.status_message("{}: {}".format(session.config.name, extract_message(params)))\n\n    def on_diagnostics_updated(self) -> None:\n        self.total_error_count = 0\n        self.total_warning_count = 0\n        for session in self._sessions:\n            local_errors, local_warnings = session.diagnostics.sum_total_errors_and_warnings_async()\n            self.total_error_count += local_errors\n            self.total_warning_count += local_warnings\n        for listener in list(self._listeners):\n            set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count)\n        if is_panel_open(self._window, PanelName.Diagnostics):\n            self.update_diagnostics_panel_async()\n\n    def update_diagnostics_panel_async(self) -> None:\n        to_render = []  # type: List[str]\n        prephantoms = []  # type: List[Tuple[int, int, str, str]]\n        row = 0\n        max_severity = userprefs().diagnostics_panel_include_severity_level\n        contributions = OrderedDict(\n        )  # type: OrderedDict[str, List[Tuple[str, Optional[int], Optional[str], Optional[str]]]]\n        for session in self._sessions:\n            for (_, path), contribution in session.diagnostics.filter_map_diagnostics_async(\n                    is_severity_included(max_severity), lambda _, diagnostic: format_diagnostic_for_panel(diagnostic)):\n                seen = path in contributions\n                contributions.setdefault(path, []).extend(contribution)\n                if not seen:\n                    contributions.move_to_end(path)\n        for path, contribution in contributions.items():\n            to_render.append("{}:".format(path))\n            row += 1\n            for content, offset, code, href in contribution:\n                to_render.append(content)\n                if offset is not None and code is not None and href is not None:\n                    prephantoms.append((row, offset, code, href))\n                row += content.count("\\n") + 1\n            to_render.append("")  # add spacing between filenames\n            row += 1\n        characters = "\\n".join(to_render)\n        if not characters:\n            characters = _NO_DIAGNOSTICS_PLACEHOLDER\n        sublime.set_timeout(functools.partial(self._update_panel_main_thread, characters, prephantoms))\n\n    def _update_panel_main_thread(self, characters: str, prephantoms: List[Tuple[int, int, str, str]]) -> None:\n        panel = ensure_diagnostics_panel(self._window)\n        if not panel or not panel.is_valid():\n            return\n        panel.run_command("lsp_update_panel", {"characters": characters})\n        if self._panel_code_phantoms is None:\n            self._panel_code_phantoms = sublime.PhantomSet(panel, "hrefs")\n        phantoms = []  # type: List[sublime.Phantom]\n        for row, col, code, href in prephantoms:\n            point = panel.text_point(row, col)\n            region = sublime.Region(point, point)\n            phantoms.append(sublime.Phantom(region, make_link(href, code), sublime.LAYOUT_INLINE))\n        self._panel_code_phantoms.update(phantoms)\n\n    def show_diagnostics_panel_async(self) -> None:\n        if self._window.active_panel() is None:\n            self._window.run_command("show_panel", {"panel": "output.diagnostics"})\n\n    def hide_diagnostics_panel_async(self) -> None:\n        if is_panel_open(self._window, PanelName.Diagnostics):\n            self._window.run_command("hide_panel", {"panel": "output.diagnostics"})\n\n\nclass WindowRegistry(object):\n    def __init__(self, configs: ConfigManager) -> None:\n        self._windows = {}  # type: Dict[int, WindowManager]\n        self._configs = configs\n\n    def lookup(self, window: sublime.Window) -> WindowManager:\n        wm = self._windows.get(window.id())\n        if wm:\n            return wm\n        workspace = ProjectFolders(window)\n        window_configs = self._configs.for_window(window)\n        state = WindowManager(window=window, workspace=workspace, configs=window_configs)\n        self._windows[window.id()] = state\n        return state\n\n    def listener_for_view(self, view: sublime.View) -> Optional[AbstractViewListener]:\n        w = view.window()\n        if not w:\n            return None\n        return self.lookup(w).listener_for_view(view)\n\n    def discard(self, window: sublime.Window) -> None:\n        self._windows.pop(window.id(), None)\n\n\nclass TrackTime:\n    def __init__(self, start: Optional[int] = None, end: Optional[int] = None) -> None:\n        self.start = start\n        self.end = end\n\n    def duration(self) -> int:\n        if not self.start and not self.end:\n            return 0\n        return self.end - self.start\n\n\nclass RequestTimeTracker:\n    def __init__(self) -> None:\n        self._request_duration = {}  # type: Dict[int, TrackTime]\n\n    def start_tracking(self, request_id: int) -> None:\n        self._request_duration[request_id] = TrackTime(start=time())\n\n    def end_tracking(self, request_id) -> None:\n        track_time = self._request_duration[request_id]\n        if track_time:\n            track_time.end = time()\n\n    def duration_in_ms(self, request_id: int) -> int:\n        duration_in_ms = 0\n        track_time = self._request_duration[request_id]\n        if track_time:\n            duration_in_ms = track_time.duration()\n        return int(duration_in_ms * 1000)\n\n    def clear(self, request_id: int) -> None:\n        self._request_duration.pop(request_id, None)\n\n\nclass PanelLogger(Logger):\n\n    def __init__(self, manager: WindowManager, server_name: str) -> None:\n        self._manager = ref(manager)\n        self._server_name = server_name\n        self.request_time_tracker = RequestTimeTracker()\n\n    def stderr_message(self, message: str) -> None:\n        """\n        Not handled here as stderr messages are handled by WindowManager regardless\n        if this logger is enabled.\n        """\n        pass\n\n    def log(self, message: str, params: Any) -> None:\n\n        def run_on_async_worker_thread() -> None:\n            nonlocal message\n            params_str = repr(params)\n            if 0 < userprefs().log_max_size <= len(params_str):\n                params_str = \'<params with {} characters>\'.format(len(params_str))\n            message = "{}: {}".format(message, params_str)\n            manager = self._manager()\n            if manager is not None:\n                manager.handle_server_message(":", message)\n\n        sublime.set_timeout_async(run_on_async_worker_thread)\n\n    def outgoing_response(self, request_id: Any, params: Any) -> None:\n        self.request_time_tracker.end_tracking(request_id)\n        if not userprefs().log_server:\n            return\n        self.log(self._format_response(">>>", request_id), params)\n        self.request_time_tracker.clear(request_id)\n\n    def outgoing_error_response(self, request_id: Any, error: Error) -> None:\n        self.request_time_tracker.end_tracking(request_id)\n        if not userprefs().log_server:\n            return\n        self.log(self._format_response("~~>", request_id), error.to_lsp())\n        self.request_time_tracker.clear(request_id)\n\n    def outgoing_request(self, request_id: int, method: str, params: Any) -> None:\n        self.request_time_tracker.start_tracking(request_id)\n        if not userprefs().log_server:\n            return\n        self.log(self._format_request("-->", method, request_id), params)\n        print(\'pedja\', self.request_time_tracker._request_duration)\n\n    def outgoing_notification(self, method: str, params: Any) -> None:\n        if not userprefs().log_server:\n            return\n        self.log(self._format_notification(" ->", method), params)\n\n    def incoming_response(self, request_id: int, params: Any, is_error: bool) -> None:\n        self.request_time_tracker.end_tracking(request_id)\n        if not userprefs().log_server:\n            return\n        direction = "<~~" if is_error else "<<<"\n        self.log(self._format_response(direction, request_id), params)\n        self.request_time_tracker.clear(request_id)\n\n    def incoming_request(self, request_id: Any, method: str, params: Any) -> None:\n        self.request_time_tracker.start_tracking(request_id)\n        if not userprefs().log_server:\n            return\n        self.log(self._format_request("<--", method, request_id), params)\n\n    def incoming_notification(self, method: str, params: Any, unhandled: bool) -> None:\n        if not userprefs().log_server:\n            return\n        direction = "<? " if unhandled else "<- "\n        self.log(self._format_notification(direction, method), params)\n\n    def _format_response(self, direction: str, request_id: Any) -> str:\n        duration_in_ms = self.request_time_tracker.duration_in_ms(request_id)\n        return "{} {} ({} | {} - {}ms)".format(direction, self._server_name, request_id, strftime("%H:%M:%S"), duration_in_ms)\n\n    def _format_request(self, direction: str, method: str, request_id: Any) -> str:\n        return "{} {} {} ({} | {})".format(direction, self._server_name, method, request_id, strftime("%H:%M:%S"))\n\n    def _format_notification(self, direction: str, method: str) -> str:\n        return "{} {} {} ({})".format(direction, self._server_name, method, strftime("%H:%M:%S"))\n\n\nclass RemoteLogger(Logger):\n    PORT = 9981\n    DIRECTION_OUTGOING = 1\n    DIRECTION_INCOMING = 2\n    _ws_server = None  # type: Optional[WebsocketServer]\n    _ws_server_thread = None  # type: Optional[threading.Thread]\n    _last_id = 0\n\n    def __init__(self, manager: WindowManager, server_name: str) -> None:\n        RemoteLogger._last_id += 1\n        self._server_name = \'{} ({})\'.format(server_name, RemoteLogger._last_id)\n        if not RemoteLogger._ws_server:\n            try:\n                RemoteLogger._ws_server = WebsocketServer(self.PORT)\n                RemoteLogger._ws_server.set_fn_new_client(self._on_new_client)\n                RemoteLogger._ws_server.set_fn_client_left(self._on_client_left)\n                RemoteLogger._ws_server.set_fn_message_received(self._on_message_received)\n                self._start_server()\n            except OSError as ex:\n                if ex.errno == 48:  # Address already in use\n                    debug(\'WebsocketServer not started - address already in use\')\n                    RemoteLogger._ws_server = None\n                else:\n                    raise ex\n\n    def _start_server(self) -> None:\n        def start_async() -> None:\n            if RemoteLogger._ws_server:\n                RemoteLogger._ws_server.run_forever()\n        RemoteLogger._ws_server_thread = threading.Thread(target=start_async)\n        RemoteLogger._ws_server_thread.start()\n\n    def _stop_server(self) -> None:\n        if RemoteLogger._ws_server:\n            RemoteLogger._ws_server.shutdown()\n            RemoteLogger._ws_server = None\n            if RemoteLogger._ws_server_thread:\n                RemoteLogger._ws_server_thread.join()\n                RemoteLogger._ws_server_thread = None\n\n    def _on_new_client(self, client: Dict, server: WebsocketServer) -> None:\n        """Called for every client connecting (after handshake)."""\n        debug("New client connected and was given id %d" % client[\'id\'])\n        # server.send_message_to_all("Hey all, a new client has joined us")\n\n    def _on_client_left(self, client: Dict, server: WebsocketServer) -> None:\n        """Called for every client disconnecting."""\n        debug("Client(%d) disconnected" % client[\'id\'])\n\n    def _on_message_received(self, client: Dict, server: WebsocketServer, message: str) -> None:\n        """Called when a client sends a message."""\n        debug("Client(%d) said: %s" % (client[\'id\'], message))\n\n    def stderr_message(self, message: str) -> None:\n        self._broadcast_json({\n            \'server\': self._server_name,\n            \'time\': round(time() * 1000),\n            \'method\': \'stderr\',\n            \'params\': message,\n            \'isError\': True,\n            \'direction\': self.DIRECTION_INCOMING,\n        })\n\n    def outgoing_request(self, request_id: int, method: str, params: Any) -> None:\n        self._broadcast_json({\n            \'server\': self._server_name,\n            \'id\': request_id,\n            \'time\': round(time() * 1000),\n            \'method\': method,\n            \'params\': params,\n            \'direction\': self.DIRECTION_OUTGOING,\n        })\n\n    def incoming_response(self, request_id: int, params: Any, is_error: bool) -> None:\n        self._broadcast_json({\n            \'server\': self._server_name,\n            \'id\': request_id,\n            \'time\': round(time() * 1000),\n            \'params\': params,\n            \'direction\': self.DIRECTION_INCOMING,\n            \'isError\': is_error,\n        })\n\n    def incoming_request(self, request_id: Any, method: str, params: Any) -> None:\n        self._broadcast_json({\n            \'server\': self._server_name,\n            \'id\': request_id,\n            \'time\': round(time() * 1000),\n            \'method\': method,\n            \'params\': params,\n            \'direction\': self.DIRECTION_INCOMING,\n        })\n\n    def outgoing_response(self, request_id: Any, params: Any) -> None:\n        self._broadcast_json({\n            \'server\': self._server_name,\n            \'id\': request_id,\n            \'time\': round(time() * 1000),\n            \'params\': params,\n            \'direction\': self.DIRECTION_OUTGOING,\n        })\n\n    def outgoing_error_response(self, request_id: Any, error: Error) -> None:\n        self._broadcast_json({\n            \'server\': self._server_name,\n            \'id\': request_id,\n            \'isError\': True,\n            \'params\': error.to_lsp(),\n            \'time\': round(time() * 1000),\n            \'direction\': self.DIRECTION_OUTGOING,\n        })\n\n    def outgoing_notification(self, method: str, params: Any) -> None:\n        self._broadcast_json({\n            \'server\': self._server_name,\n            \'time\': round(time() * 1000),\n            \'method\': method,\n            \'params\': params,\n            \'direction\': self.DIRECTION_OUTGOING,\n        })\n\n    def incoming_notification(self, method: str, params: Any, unhandled: bool) -> None:\n        self._broadcast_json({\n            \'server\': self._server_name,\n            \'time\': round(time() * 1000),\n            \'error\': \'Unhandled notification!\' if unhandled else None,\n            \'method\': method,\n            \'params\': params,\n            \'direction\': self.DIRECTION_INCOMING,\n        })\n\n    def _broadcast_json(self, data: Dict[str, Any]) -> None:\n        if RemoteLogger._ws_server:\n            json_data = json.dumps(data, sort_keys=True, check_circular=False, separators=(\',\', \':\'))\n            RemoteLogger._ws_server.send_message_to_all(json_data)\n\n\nclass RouterLogger(Logger):\n    def __init__(self) -> None:\n        self._loggers = []  # type: List[Logger]\n\n    def append(self, logger: Logger) -> None:\n        self._loggers.append(logger)\n\n    def stderr_message(self, *args: Any, **kwargs: Any) -> None:\n        self._foreach("stderr_message", *args, **kwargs)\n\n    def outgoing_response(self, *args: Any, **kwargs: Any) -> None:\n        self._foreach("outgoing_response", *args, **kwargs)\n\n    def outgoing_error_response(self, *args: Any, **kwargs: Any) -> None:\n        self._foreach("outgoing_error_response", *args, **kwargs)\n\n    def outgoing_request(self, *args: Any, **kwargs: Any) -> None:\n        self._foreach("outgoing_request", *args, **kwargs)\n\n    def outgoing_notification(self, *args: Any, **kwargs: Any) -> None:\n        self._foreach("outgoing_notification", *args, **kwargs)\n\n    def incoming_response(self, *args: Any, **kwargs: Any) -> None:\n        self._foreach("incoming_response", *args, **kwargs)\n\n    def incoming_request(self, *args: Any, **kwargs: Any) -> None:\n        self._foreach("incoming_request", *args, **kwargs)\n\n    def incoming_notification(self, *args: Any, **kwargs: Any) -> None:\n        self._foreach("incoming_notification", *args, **kwargs)\n\n    def _foreach(self, method: str, *args: Any, **kwargs: Any) -> None:\n        for logger in self._loggers:\n            getattr(logger, method)(*args, **kwargs)\n', 'languageId': 'python', 'version': 0, 'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: --> LSP-pylsp textDocument/codeLens (2 | 12:18:03): {'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: <<< LSP-pylsp (2 | 12:18:03 - 26ms): []
:: --> LSP-pylsp textDocument/hover (3 | 12:18:03): {'position': {'line': 561, 'character': 20}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: --> LSP-pylsp textDocument/codeAction (4 | 12:18:03): {'range': {'start': {'line': 561, 'character': 20}, 'end': {'line': 561, 'character': 20}}, 'context': {'diagnostics': [], 'triggerKind': 2}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: --> LSP-pylsp textDocument/hover (5 | 12:18:04): {'position': {'line': 556, 'character': 23}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: --> LSP-pylsp textDocument/codeAction (6 | 12:18:04): {'range': {'start': {'line': 556, 'character': 23}, 'end': {'line': 556, 'character': 23}}, 'context': {'diagnostics': [], 'triggerKind': 2}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: <-  LSP-pylsp textDocument/publishDiagnostics (12:18:04): {'diagnostics': [{'range': {'start': {'line': 613, 'character': 120}, 'end': {'line': 613, 'character': 127}}, 'source': 'pycodestyle', 'message': 'E501 line too long (126 > 120 characters)', 'severity': 2, 'code': 'E501'}], 'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}
:: --> LSP-pylsp textDocument/codeAction (7 | 12:18:04): {'range': {'start': {'line': 564, 'character': 0}, 'end': {'line': 564, 'character': 0}}, 'context': {'diagnostics': [], 'triggerKind': 2}, 'textDocument': {'uri': 'file:///home/predragnikolic/.config/sublime-text/Packages/LSP/plugin/core/windows.py'}}
:: <<< LSP-pylsp (3 | 12:18:04 - 1552ms): {'contents': ''}
:: <<< LSP-pylsp (4 | 12:18:04 - 1546ms): []
:: <<< LSP-pylsp (5 | 12:18:04 - 429ms): {'contents': [{'value': 'userprefs() -> Settings', 'language': 'python'}]}
:: <<< LSP-pylsp (6 | 12:18:04 - 422ms): []
:: <<< LSP-pylsp (7 | 12:18:04 - 396ms): []
```

> NOTE: Responses also have a time duration

https://github.com/sublimelsp/LSP/tree/timestamps-in-logs 
- I'd put timestamp before server name so that it's aligned and easy to ignore if you are not looking for it
- Should have millisecond precision like `12:18:02.433`
- Ideally pad it to certain amount of characters so that all timestamps are aligned.
- The "duration" could be separate

Resolving patch:
diff --git a/plugin/core/windows.py b/plugin/core/windows.py
--- a/plugin/core/windows.py
+++ b/plugin/core/windows.py
@@ -32,8 +32,9 @@
 from .workspace import sorted_workspace_folders
 from collections import deque
 from collections import OrderedDict
+from datetime import datetime
 from subprocess import CalledProcessError
-from time import time
+from time import perf_counter
 from weakref import ref
 from weakref import WeakSet
 import functools
@@ -540,11 +541,33 @@ def discard(self, window: sublime.Window) -> None:
             wm.destroy()
 
 
+class RequestTimeTracker:
+    def __init__(self) -> None:
+        self._start_times = {}  # type: Dict[int, float]
+
+    def start_tracking(self, request_id: int) -> None:
+        self._start_times[request_id] = perf_counter()
+
+    def end_tracking(self, request_id) -> str:
+        duration = '-'
+        if request_id in self._start_times:
+            start = self._start_times.pop(request_id)
+            duration_ms = perf_counter() - start
+            duration = '{}ms'.format(int(duration_ms * 1000))
+        return duration
+
+    @classmethod
+    def formatted_now(cls) -> str:
+        now = datetime.now()
+        return '{}.{:03d}'.format(now.strftime("%H:%M:%S"), int(now.microsecond / 1000))
+
+
 class PanelLogger(Logger):
 
     def __init__(self, manager: WindowManager, server_name: str) -> None:
         self._manager = ref(manager)
         self._server_name = server_name
+        self._request_time_tracker = RequestTimeTracker()
 
     def stderr_message(self, message: str) -> None:
         """
@@ -570,16 +593,19 @@ def run_on_async_worker_thread() -> None:
     def outgoing_response(self, request_id: Any, params: Any) -> None:
         if not userprefs().log_server:
             return
-        self.log(self._format_response(">>>", request_id), params)
+        duration = self._request_time_tracker.end_tracking(request_id)
+        self.log(self._format_response(">>>", request_id, duration), params)
 
     def outgoing_error_response(self, request_id: Any, error: Error) -> None:
         if not userprefs().log_server:
             return
-        self.log(self._format_response("~~>", request_id), error.to_lsp())
+        duration = self._request_time_tracker.end_tracking(request_id)
+        self.log(self._format_response("~~>", request_id, duration), error.to_lsp())
 
     def outgoing_request(self, request_id: int, method: str, params: Any) -> None:
         if not userprefs().log_server:
             return
+        self._request_time_tracker.start_tracking(request_id)
         self.log(self._format_request("-->", method, request_id), params)
 
     def outgoing_notification(self, method: str, params: Any) -> None:
@@ -591,11 +617,13 @@ def incoming_response(self, request_id: int, params: Any, is_error: bool) -> Non
         if not userprefs().log_server:
             return
         direction = "<~~" if is_error else "<<<"
-        self.log(self._format_response(direction, request_id), params)
+        duration = self._request_time_tracker.end_tracking(request_id)
+        self.log(self._format_response(direction, request_id, duration), params)
 
     def incoming_request(self, request_id: Any, method: str, params: Any) -> None:
         if not userprefs().log_server:
             return
+        self._request_time_tracker.start_tracking(request_id)
         self.log(self._format_request("<--", method, request_id), params)
 
     def incoming_notification(self, method: str, params: Any, unhandled: bool) -> None:
@@ -604,14 +632,16 @@ def incoming_notification(self, method: str, params: Any, unhandled: bool) -> No
         direction = "<? " if unhandled else "<- "
         self.log(self._format_notification(direction, method), params)
 
-    def _format_response(self, direction: str, request_id: Any) -> str:
-        return "{} {} {}".format(direction, self._server_name, request_id)
+    def _format_response(self, direction: str, request_id: Any, duration: str) -> str:
+        return "[{}] {} {} ({}) (duration: {})".format(
+            RequestTimeTracker.formatted_now(), direction, self._server_name, request_id, duration)
 
     def _format_request(self, direction: str, method: str, request_id: Any) -> str:
-        return "{} {} {}({})".format(direction, self._server_name, method, request_id)
+        return "[{}] {} {} {} ({})".format(
+            RequestTimeTracker.formatted_now(), direction, self._server_name, method, request_id)
 
     def _format_notification(self, direction: str, method: str) -> str:
-        return "{} {} {}".format(direction, self._server_name, method)
+        return "[{}] {} {} {}".format(RequestTimeTracker.formatted_now(), direction, self._server_name, method)
 
 
 class RemoteLogger(Logger):
@@ -670,7 +700,7 @@ def _on_message_received(self, client: Dict, server: WebsocketServer, message: s
     def stderr_message(self, message: str) -> None:
         self._broadcast_json({
             'server': self._server_name,
-            'time': round(time() * 1000),
+            'time': round(perf_counter() * 1000),
             'method': 'stderr',
             'params': message,
             'isError': True,
@@ -681,7 +711,7 @@ def outgoing_request(self, request_id: int, method: str, params: Any) -> None:
         self._broadcast_json({
             'server': self._server_name,
             'id': request_id,
-            'time': round(time() * 1000),
+            'time': round(perf_counter() * 1000),
             'method': method,
             'params': params,
             'direction': self.DIRECTION_OUTGOING,
@@ -691,7 +721,7 @@ def incoming_response(self, request_id: int, params: Any, is_error: bool) -> Non
         self._broadcast_json({
             'server': self._server_name,
             'id': request_id,
-            'time': round(time() * 1000),
+            'time': round(perf_counter() * 1000),
             'params': params,
             'direction': self.DIRECTION_INCOMING,
             'isError': is_error,
@@ -701,7 +731,7 @@ def incoming_request(self, request_id: Any, method: str, params: Any) -> None:
         self._broadcast_json({
             'server': self._server_name,
             'id': request_id,
-            'time': round(time() * 1000),
+            'time': round(perf_counter() * 1000),
             'method': method,
             'params': params,
             'direction': self.DIRECTION_INCOMING,
@@ -711,7 +741,7 @@ def outgoing_response(self, request_id: Any, params: Any) -> None:
         self._broadcast_json({
             'server': self._server_name,
             'id': request_id,
-            'time': round(time() * 1000),
+            'time': round(perf_counter() * 1000),
             'params': params,
             'direction': self.DIRECTION_OUTGOING,
         })
@@ -722,14 +752,14 @@ def outgoing_error_response(self, request_id: Any, error: Error) -> None:
             'id': request_id,
             'isError': True,
             'params': error.to_lsp(),
-            'time': round(time() * 1000),
+            'time': round(perf_counter() * 1000),
             'direction': self.DIRECTION_OUTGOING,
         })
 
     def outgoing_notification(self, method: str, params: Any) -> None:
         self._broadcast_json({
             'server': self._server_name,
-            'time': round(time() * 1000),
+            'time': round(perf_counter() * 1000),
             'method': method,
             'params': params,
             'direction': self.DIRECTION_OUTGOING,
@@ -738,7 +768,7 @@ def outgoing_notification(self, method: str, params: Any) -> None:
     def incoming_notification(self, method: str, params: Any, unhandled: bool) -> None:
         self._broadcast_json({
             'server': self._server_name,
-            'time': round(time() * 1000),
+            'time': round(perf_counter() * 1000),
             'error': 'Unhandled notification!' if unhandled else None,
             'method': method,
             'params': params,


Test patch: