Skip to content

Bench/issue 2296 completion benchmark#4178

Open
kz357 wants to merge 2 commits into
facebook:mainfrom
kz357:bench/issue-2296-completion-benchmark
Open

Bench/issue 2296 completion benchmark#4178
kz357 wants to merge 2 commits into
facebook:mainfrom
kz357:bench/issue-2296-completion-benchmark

Conversation

@kz357

@kz357 kz357 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow up to PR #4148, per maintainer suggestion. Completions benchmark to catch future regressions on this path.

Fix short reads of large LSP responses-- client spawns servers with bufsize=0 so stdout is a raw io.FileIO whose read(n) is a single syscall returning at most one pipe buffer instead of n bytes. _rx_loop assumed read(length) returned full body, so any response longer than pipe buffer was truncated, failed to parse, and desynced the stream, and the request would time out even though the server had already answered it. Go to Definition responses are small so this never triggered, but a completion list is big enough that it would be problematic (verified through testing). Now it reads the body in a loop.

ADd --mode {definition,completion}-- in completion mode each run picks a member access on an imported module (eg pl.DataFrame after import polars as pl), truncates member to --prefix-len chars, and measures textDocument/completion at that point, so the server sees a partially-typed member access (eg pl.D in this case) as it would while typing

Server derives its prefix filter from the whole identifier under the cursor, so putting the cursor inside an existing complete token would filter on the full name rather than a short prefix, and wouldn't reproduce the scenario in #2296, so truncation is necessary

Original member is known, so benchmark also verifies that the server offered it back, which is what valid col reports in completion mode, so this measures correctness as well as latency

Multi-package mode re-enters main() with a constructed argv, so it picks up new flags by forwarding them through so completion works in both single-repo and multi-package mode without a second benchmark loop.

Related: issue #2296

Test Plan

Ran both modes against a small local repo (import os / import json with member accesses), using a pyrefly LSP on my local computer:

  • --mode completion --runs 3 --seed 42 gave ok=3/3, valid=3/3. The valid column confirms pyrefly returned getcwd when given only os.g.
  • Default (definition) mode, same repo/seed, compared against the script from upstream/main gave identical results (ok=3/3, valid=0/3, same cases and latency modulo noise), so shared code paths are unchanged. valid=0/3 there is pre-existing and the only symbols in that repo are stdlib, whose definitions resolve into bundled typeshed and so aren't on disk for the existing _looks_like_valid_location check

Before the short-read fix, every completion request timed out at 10s despite server logging that it happened in ~0.2s, which is how the bug came up.

Mentioned latencies are from a debug build and not real but I think they're proof that the benchmark works :D

kz357 added 2 commits July 17, 2026 02:07
LSP client spawns servers with bufsize=0, so stdout is a raw io.FileIO whose read(n) is a single syscall returning at most one pipe buffer, not n bytes. _rx_loop assumed read(length) returned whole body, so any response larger than pipe buffer came back truncated, failed to parse, and desynced the stream. Then the request would time out even though the server had already answered it fine.

Go to Definition responses are small enough that this never happened, but I came across it while writing testcase. Reads the body in a loop instead
Adds --mode {definition,completion} to lsp_benchmark.py. In completion mode each run picks a member access on an imported module (eg pl.DataFrame after import polars as pl), truncates member to --prefix-len characters, and measures textDocument/completion at that point, so the server sees a partially-typed member access as it would while typing.

Server derives its filter prefix from the whole identifier under the cursor, so parking the cursor inside an existing complete token would filter on the full name instead of a short prefix. So truncation matters

Bc original member name is known, benchmark also checks that the server offered it back, which is what `valid` col reports in completion mode

Multi-package mode re-enters main() with a constructed argv so it picks up the new flags by forwarding them through

Follow-up to PR facebook#4148, per maintainer suggestion
Related: issue facebook#2296
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant