Skip to content

fix(server): don't busy-wait for the first client login#2268

Merged
cxxxr merged 1 commit into
mainfrom
fix/server-busy-wait-on-startup
Jul 15, 2026
Merged

fix(server): don't busy-wait for the first client login#2268
cxxxr merged 1 commit into
mainfrom
fix/server-busy-wait-on-startup

Conversation

@cxxxr

@cxxxr cxxxr commented Jul 15, 2026

Copy link
Copy Markdown
Member

Problem

In the JSONRPC (lem-server) frontend, the editor thread's initialize callback spins in a bare (loop :until ready) until the first client calls login. This pins one CPU core at 100% from process startup. On a server deployment where lem-server runs continuously and no client is connected yet, the process burns a full core indefinitely — it shows up as a "hung" container at 100% CPU.

The busy-wait has been there since lem-server was introduced (2f45f2f). Disassembly on SBCL 2.6.3 shows the loop compiles to a 3-instruction tight spin (value-cell read → NIL compare → jump).

Fix

Replace the busy-wait flag with a bt2:semaphore, so the editor thread blocks in wait-on-semaphore instead of spinning. The login callback signals the semaphore. Signaling more than once (e.g. when another client logs in later) just increments the count and is harmless.

Verification

Ran lem-server:run-websocket-server locally and measured per-thread CPU with ps -eL:

state editor thread CPU
before fix, idle (no client) ~93%
after fix, idle (no client) ~0%
after fix, after login ~0% (editor loop running normally)

Also verified over an actual WebSocket connection that the JSON-RPC login request still returns a normal response and unblocks the editor thread.

🤖 Generated with Claude Code

The editor thread's initialize callback spun in a bare (loop :until
ready) until the first client called "login", pinning one CPU core at
100% from startup. On a deployed server with no connected client, the
process burned a full core indefinitely.

Replace the busy-wait flag with a semaphore so the editor thread
blocks in wait-on-semaphore instead of spinning. Signaling more than
once (e.g. on client reconnect) is harmless.

Measured with the websocket server idle before login:
before: editor thread at ~93% CPU
after:  editor thread at ~0% CPU, login still unblocks it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@code-contractor-app

code-contractor-app Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

✅ Code Contractor Validation: PASSED

=== Contract: contract ===

✓ Code Contractor Validation Result
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📋 Contract Source: Repository

📊 Statistics:
  Files Changed:    1
  Lines Added:      3
  Lines Deleted:    3
  Total Changed:    6
  Delete Ratio:     0.50 (50%)

Status: PASSED ✅

🤖 AI Providers:
  - codex — model: gpt-5.4

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎉 No violations detected. Great job!
📋 Contract Configuration: contract (Source: Repository)
version: 2

trigger:
  paths:
    - "extensions/**"
    - "frontends/**/*.lisp"
    - "src/**"
    - "tests/**"
    - "contrib/**"
    - "**/*.asd"
  head_branches:
    exclude:
      - 'revert-*'

validation:
  limits:
    max_total_changed_lines: 400
    max_delete_ratio: 0.5
    max_files_changed: 10
    severity: warning

  ai:
    system_prompt: |
      You are a senior Common Lisp engineer reviewing code for Lem editor.
      Lem is a text editor with multiple frontends (ncurses, SDL2, webview).
      Focus on maintainability, consistency with existing code, and Lem-specific conventions.
    rules:
      # === File Structure ===
      - name: defpackage_rule
        prompt: |
          First form must be `defpackage` or `uiop:define-package`.
          Package name should match filename (e.g., `foo.lisp` → `:lem-ext/foo` or `:lem-foo`).
          Extensions must use `lem-` prefix (e.g., `:lem-python-mode`).

      - name: file_structure_rule
        prompt: |
          File organization (top to bottom):
          1. defpackage
          2. defvar/defparameter declarations
          3. Key bindings (define-key, define-keys)
          4. Class/struct definitions
          5. Functions and commands

          Only flag violations when elements appear OUT OF ORDER (e.g., key bindings AFTER functions, or defvar AFTER functions).
          Key bindings appearing BEFORE functions is correct and expected.

      # === Style ===
      - name: loop_keywords_rule
        prompt: |
          Loop keywords must use colons: `(loop :for x :in list :do ...)`
          NOT: `(loop for x in list do ...)`

      - name: naming_conventions_rule
        prompt: |
          Naming conventions:
          - Functions/variables: kebab-case (e.g., `find-buffer`)
          - Special variables: *earmuffs* (e.g., `*global-keymap*`)
          - Constants: +plus-signs+ (e.g., `+default-tab-size+`)
          - Predicates: -p suffix for functions (e.g., `buffer-modified-p`)
          - Do NOT use -p suffix for user-configurable variables

      # === Documentation ===
      - name: docstring_rule
        prompt: |
          Required docstrings for:
          - Exported functions, methods, classes
          - `define-command` (explain what the command does)
          - Generic functions (`:documentation` option)
          Important functions should explain "why", not just "what".
        severity: warning

      # === Lem-Specific ===
      - name: internal_symbol_rule
        prompt: |
          Use exported symbols from `lem` or `lem-core` package.
          Avoid `lem::internal-symbol` access.
          If internal access is necessary, document why.

      - name: error_handling_rule
        prompt: |
          - `error`: Internal/programming errors
          - `editor-error`: User-facing errors (displayed in echo area)
          Always use `editor-error` for messages shown to users.

      - name: frontend_interface_rule
        prompt: |
          Frontend-specific code must use `lem-if:*` protocol.
          Do not call frontend implementation directly from core.
        severity: warning

      # === Functional Style ===
      - name: functional_style_rule
        prompt: |
          Prefer explicit function arguments over dynamic variables.
          Avoid using `defvar` for state passed between functions.
          Exception: Well-documented cases like `*current-buffer*`.

      - name: dynamic_symbol_call_rule
        prompt: |
          Avoid `uiop:symbol-call`. Rethink architecture instead.
          If unavoidable, document the reason.

      # === Libraries ===
      - name: alexandria_usage_rule
        prompt: |
          Alexandria utilities allowed: `if-let`, `when-let`, `with-gensyms`, etc.
          Avoid: `alexandria:curry` (use explicit lambdas)
          Avoid: `alexandria-2:*` functions not yet used in codebase

      # === Macros ===
      - name: macro_style_rule
        prompt: |
          Keep macros small. For complex logic, use `call-with-*` pattern:
          ```lisp
          (defmacro with-foo (() &body body)
            `(call-with-foo (lambda () ,@body)))
          ```
          Prefer `list` over backquote outside macros.
📚 About Code Contractor

Declarative Code Standards That Learn and Improve

Define domain-specific validation rules in YAML.
Your contracts document team knowledge and evolve into more accurate AI enforcement.

Want this for your repo?
Install Code Contractor

@cxxxr cxxxr merged commit 1d8b517 into main Jul 15, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant