Skip to content

Optimize LSP discriminated union decoding - #63934

Draft
Jake Bailey (jakebailey) wants to merge 3 commits into
microsoft:mainfrom
jakebailey:jakebailey/lsp-streaming-discriminated-unions
Draft

Optimize LSP discriminated union decoding#63934
Jake Bailey (jakebailey) wants to merge 3 commits into
microsoft:mainfrom
jakebailey:jakebailey/lsp-streaming-discriminated-unions

Conversation

@jakebailey

@jakebailey Jake Bailey (jakebailey) commented Aug 20, 2026

Copy link
Copy Markdown
Member

While doing some JSON stuff, I realized that since the LSP client put the discriminator first, we can skip buffering anything and just go right into the real decode. If the discriminator is not first, then we are forced to do the slow thing, but the VS Code language client always puts the discriminator first!

This nets faster decoding:

goos: linux
goarch: amd64
pkg: github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto
cpu: AMD Ryzen AI 9 HX 370 w/ Radeon 890M           
                                                             │    old.txt    │               new.txt               │
                                                             │    sec/op     │   sec/op     vs base                │
UnmarshalDiscriminatedUnion/discriminator-first/buffered-24     1.339µ ±  5%   1.381µ ± 6%        ~ (p=0.247 n=10)
UnmarshalDiscriminatedUnion/discriminator-first/streaming-24   1275.5n ± 10%   745.5n ± 5%  -41.55% (p=0.000 n=10)
UnmarshalDiscriminatedUnion/discriminator-last/buffered-24      1.710µ ±  8%   1.843µ ± 5%   +7.78% (p=0.023 n=10)
UnmarshalDiscriminatedUnion/discriminator-last/streaming-24     1.560µ ±  6%   1.262µ ± 6%  -19.10% (p=0.000 n=10)
geomean                                                         1.461µ         1.244µ       -14.85%
                                                             │  old.txt   │               new.txt                │
                                                             │    B/op    │    B/op     vs base                  │
UnmarshalDiscriminatedUnion/discriminator-first/buffered-24    624.0 ± 0%   624.0 ± 0%        ~ (p=1.000 n=10) ¹
UnmarshalDiscriminatedUnion/discriminator-first/streaming-24   560.0 ± 0%   184.0 ± 0%  -67.14% (p=0.000 n=10)
UnmarshalDiscriminatedUnion/discriminator-last/buffered-24     857.0 ± 0%   857.0 ± 0%        ~ (p=1.000 n=10) ¹
UnmarshalDiscriminatedUnion/discriminator-last/streaming-24    785.0 ± 0%   536.0 ± 0%  -31.72% (p=0.000 n=10)
geomean                                                        696.3        479.2       -31.18%
¹ all samples are equal
                                                             │  old.txt   │               new.txt               │
                                                             │ allocs/op  │ allocs/op   vs base                 │
UnmarshalDiscriminatedUnion/discriminator-first/buffered-24    13.00 ± 0%   13.00 ± 0%       ~ (p=1.000 n=10) ¹
UnmarshalDiscriminatedUnion/discriminator-first/streaming-24   12.00 ± 0%   11.00 ± 0%  -8.33% (p=0.000 n=10)
UnmarshalDiscriminatedUnion/discriminator-last/buffered-24     20.00 ± 0%   20.00 ± 0%       ~ (p=1.000 n=10) ¹
UnmarshalDiscriminatedUnion/discriminator-last/streaming-24    18.00 ± 0%   18.00 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                        15.39        15.06       -2.15%
¹ all samples are equal

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes LSP discriminated-union decoding by streaming fields once the discriminator is found.

Changes:

  • Adds reusable streaming struct decoding.
  • Updates generated union decoders and generator logic.
  • Adds correctness tests and benchmarks.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
structcodec.go Implements streaming discriminated-struct decoding.
lsp.go Removes obsolete raw-field scanning.
lsp_json_test.go Tests discriminator ordering and absence.
lsp_json_benchmark_test.go Benchmarks buffered versus streaming decoding.
lsp_generated.go Uses streaming decoding in generated unions.
_generate/generate.mts Generates streaming discriminator dispatch.
Files not reviewed (1)
  • tsc/internal/lsp/lsproto/lsp_generated.go: Generated file

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread tsc/internal/lsp/lsproto/structcodec.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • tsc/internal/lsp/lsproto/lsp_generated.go: Generated file
Suppressed comments (2)

tsc/internal/lsp/lsproto/_generate/generate.mts:3462

  • This path can stream only when the optional fallback arm is a generated structure. In particular, when hasUnknownKinds comes from an any arm, non-object input now fails the object scan instead of decoding as any, while object input reaching the default arm can panic when unmarshalDiscriminatedArm reflects over the non-struct. Maps are another non-struct object fallback with the same panic. Retain the buffered discriminator dispatch for such arms.
            if (disc && disc.unmapped.length <= 1) {
                generateStreamingDiscriminatorDispatch(name, disc, "\t");
                exhaustive = true;

tsc/internal/lsp/lsproto/structcodec.go:212

  • Do not reject non-string values here: a generated union may have one unmapped fallback arm. For example, a valid TextDocumentEdit can contain an unknown "kind": null property; the previous decoder dispatched its raw value to the default TextDocumentEdit arm, whose struct decoder ignores that property, but this scan now fails before reaching the fallback. Retain the raw value and let the generated switch either choose its fallback or report an invalid discriminator when no fallback exists.
			if value.Kind() != '"' {
				return discriminatedStructDecoder{}, fmt.Errorf("invalid %s discriminator %q: got %v", typeName, discriminator, value.Kind())
			}

Comment on lines +3415 to +3417
if (disc && disc.unmapped.length <= 1) {
generateStreamingDiscriminatorDispatch(name, disc, "\t\t");
exhaustive = true;
@jakebailey
Jake Bailey (jakebailey) force-pushed the jakebailey/lsp-streaming-discriminated-unions branch from c2a0f70 to 66d7e1c Compare August 21, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

2 participants