원문 캡처 · jamescherti.com
원문 캡처 · jamescherti.com

Emacs에 기본 포함된 경량 LSP 클라이언트 Eglot은 대부분의 프로젝트에서 기본 설정으로 충분하지만, 대형 코드베이스에서는 지연이 생길 수 있다. 이 글은 Eglot과 Emacs의 작업 부담을 줄이는 설정을 소개하며, 실제 효과는 작업량과 언어 서버에 따라 달라질 수 있다고 설명한다.

사용하지 않는 버퍼를 닫았을 때 언어 서버도 종료하려면 다음과 같이 설정한다.

(setq eglot-autoshutdown t)

이 설정은 관리 중인 마지막 버퍼가 닫힌 뒤 LSP 서버를 종료한다. 프로젝트를 여러 개 오가면서 버퍼를 닫아도 별도의 언어 서버가 계속 실행되는 상황을 줄이는 데 쓸 수 있다. 버퍼 정리를 자동화하려면 buffer-terminator 패키지를 활용할 수 있다.

프로젝트를 열 때 언어 서버 초기화를 기다리느라 Emacs가 멈추는 것을 피하려면 초기 연결을 비동기로 진행하도록 설정한다.

(setq eglot-sync-connect nil)

Eglot은 JSON-RPC 활동을 이벤트 버퍼에 기록한다. 로그가 커지면 메모리를 쓰고 문자열 할당 부담도 늘 수 있다. 이벤트 버퍼에 보관하는 데이터 양은 다음 설정으로 줄일 수 있다. Emacs 29 이하에서는 eglot-events-buffer-size를 사용한다.

(setq eglot-events-buffer-config '(:size 0 :format short))

:size 0은 이벤트 버퍼를 비활성화한다.

파일 감시 범위를 제한하면 파일 디스크립터와 메모리 사용량, 시작 지연을 줄이는 데 도움이 될 수 있다.

(setq eglot-max-file-watches 5000)

언어 서버가 지원한다면 node_modules, .git, 빌드 출력 디렉터리처럼 큰 폴더를 감시 대상에서 제외할 수도 있다. 제외 규칙은 언어 서버마다 다르므로 eglot-workspace-configuration을 통해 전달해야 한다. 저장소 루트의 .dir-locals.el 파일에 프로젝트별 규칙을 정의하는 방법도 있다.

진행 상황 보고는 모드 라인의 다시 그리기를 일으킬 수 있다. 이를 끄려면 다음과 같이 설정한다.

(setq eglot-report-progress nil)

커서가 멈춰 있을 때 코드 액션을 자동으로 조회하는 기능은 다음 설정으로 끌 수 있다.

(setq eglot-code-action-indications nil)

이 설정은 자동 표시만 끈다. M-x eglot-code-actions를 실행해 코드 액션을 직접 요청하고 적용하는 기능은 계속 사용할 수 있다.

언어 서버의 선택 기능 가운데 쓰지 않는 것만 비활성화할 수 있다. 실제 성능 효과는 언어 서버와 프로젝트에 따라 다르다. 입력할 때마다 형식 지정 가능 여부를 확인하는 작업을 막으려면 다음 설정을 사용한다.

(with-eval-after-load 'eglot (add-to-list 'eglot-ignored-server-capabilities :documentOnTypeFormattingProvider))

인레이 힌트를 끄면 버퍼에 자동으로 표시되는 타입과 매개변수 이름 주석을 없앤다.

(with-eval-after-load 'eglot (add-to-list 'eglot-ignored-server-capabilities :inlayHintProvider))

커서 아래 기호와 같은 이름을 강조하는 기능은 다음 설정으로 끌 수 있다.

(with-eval-after-load 'eglot (add-to-list 'eglot-ignored-server-capabilities :documentHighlightProvider))

언어 서버가 제공하는 시맨틱 토큰을 끄면 관련 통신과 처리 작업을 줄일 수 있다. 다만 일부 언어에서는 구문 강조가 덜 정밀하거나 상세해질 수 있다. Tree-sitter의 구문 강조와 LSP 시맨틱 토큰은 서로 같지 않다.

(with-eval-after-load 'eglot (add-to-list 'eglot-ignored-server-capabilities :semanticTokensProvider))

가비지 컬렉션(GC) 임계값을 높이면 수집 빈도를 낮출 수 있지만 효과는 작업량에 따라 달라진다.

(setq gc-cons-threshold (* 100 1024 1024))

대안으로 gcmh 패키지를 사용하면 편집 중 임계값을 높이고 Emacs가 유휴 상태가 됐을 때 가비지 컬렉션을 수행할 수 있다.

네이티브 컴파일을 지원하도록 빌드된 Emacs에서는 Emacs Lisp 코드 실행 속도가 개선될 수 있다. 모든 패키지를 네이티브 컴파일하도록 관리하는 도구로 compile-angel을 권한다.

read-process-output-max는 Emacs가 한 번의 작업에서 하위 프로세스로부터 읽는 데이터의 최대량을 정한다. 언어 서버가 초기화나 작업 공간 색인 중 큰 JSON-RPC 데이터를 한꺼번에 보낼 때 값을 높이면 읽기 작업 횟수를 줄일 수 있다.

(setq read-process-output-max (* 1024 1024))

이 설정은 한 번에 읽을 수 있는 한도를 1MiB로 높인다. 프로세스마다 1MiB 메모리를 예약하거나 서버가 보낼 수 있는 데이터 양을 늘리는 것은 아니다. GNU/Linux에서는 /proc/sys/fs/pipe-max-size를 초과하지 않아야 한다.

Emacs는 Lisp 실행, 비동기 프로세스 출력 처리, 화면 갱신을 하나의 주 스레드에서 수행한다. 기존 메이저 모드에서는 입력할 때 복잡한 정규식을 계산해 구문 강조를 할 수 있으며, 이 작업이 Eglot의 응답 처리와 같은 스레드를 사용해 지연을 일으킬 수 있다. 언어에 안정적인 *-ts-mode가 있다면 c-ts-mode, python-ts-mode, rust-ts-mode 같은 Tree-sitter 기반 모드를 사용해 볼 수 있다. 이 모드는 Tree-Sitter C 라이브러리로 구문을 점진적으로 분석한다.