Skip to content

uri: canonical vscode-uri rewrite with LSP-optimized hot path - #4

Merged
zchee merged 10 commits into
mainfrom
fastest
Jun 11, 2026
Merged

uri: canonical vscode-uri rewrite with LSP-optimized hot path#4
zchee merged 10 commits into
mainfrom
fastest

Conversation

@zchee

@zchee zchee commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

This branch replaces the previous net/url-backed implementation of go.lsp.dev/uri with a canonical vscode-uri-compatible parser and formatter, optimized for the LSP hot path. It also migrates CI/tooling (CircleCI -> GitHub Actions, golangci-lint v2) and adds a reproducible benchmark + conformance-vector harness.

This is an intentional behavior rewrite with breaking API changes. See docs/migration.md for the full migration guide.

Motivation

The old New/Filename/From(scheme, ...) surface mixed URI parsing with filesystem fallback (os.Getwd, filepath.Abs, runtime.GOROOT) and could emit invalid URIs. The rewrite aligns Go identity semantics with VSCode's vscode-uri so that file://SERVER/x == file://server/x and file:///C:/x == file:///c%3A/x compare equal and are safe as native Go == and map keys.

Breaking API changes

Removed / changed Replacement
New(string) Parse(string) (URI text) or File(string) (filesystem path)
Filename() FsPath() / FsPathFor(URI, Platform, bool)
From(scheme, authority, path, query, fragment) From(Components)
FileScheme, HTTPScheme, HTTPSScheme Scheme(), IsFile()

Key behavioral differences:

  • Parse follows vscode-uri non-strict parsing (empty-scheme falls back to file); ParseStrict requires a scheme and returns typed sentinel errors.
  • File/FileFor never touch the filesystem; relative-looking input is encoded as provided.
  • Empty query/fragment delimiters are canonicalized away, matching URI.parse(input).toString().
  • type URI string is preserved, so direct URI("...") conversions still compile — but they do not validate or canonicalize. Prefer constructors for new values.
  • Constructor-produced values compare by canonical-string identity; component accessors expose the canonical (normalized-casing) view.

Design: string-backed canonical values

The public representation stays type URI string. Equality and map keys are free native ==; String() is a 2-cost inlinable conversion. Component accessors (Path, Authority, ...) split the canonical string on demand rather than caching per-value offsets, keeping the hot path allocation-free.

Performance

Headline numbers (Apple M3 Max, darwin/arm64, go1.26.4, GOEXPERIMENT unset). Full evidence + reproduction in docs/perf.md.

Workload Result Target Status
Parse("file:///.../main.go") 11.93 ns/op, 0 allocs <= 40 ns/op, 0 allocs PASS
FsPathFor clean POSIX file URI 14.45 ns/op, 0 allocs <= 15 ns/op, 0 allocs PASS
String() 1.883 ns/op, 0 allocs <= 2 ns/op, 0 allocs PASS
FileFor clean absolute path 29.67 ns/op, 1 alloc <= 60 ns/op, <= 1 alloc PASS
Map-key insert+lookup vs net/url string baseline >= 2x faster PASS (4.10x)
Parse("https://host/p?name=ferret#f") 123.3 ns/op, 1 alloc <= 80 ns/op, <= 2 allocs latency GAP (alloc PASS)

Known gaps (documented, not regressions)

  1. Dirty HTTP(S) parse latency (123.3 ns/op) is above the aspirational 80 ns/op target; allocation is already under target.
  2. Percent-encoded file paths and Windows/UNC conversion still allocate (decode, lowercase drive, prepend UNC authority, replace slashes).
  3. Component accessors derive on demand — zero-alloc but slower than cached offsets would be.

Tooling / CI

  • CircleCI -> GitHub Actions (ubuntu-slim + ubuntu-24.04-arm), Codecov via OIDC, lint via golangci-lint v2.
  • Conformance vectors regenerated from a pinned Node vscode-uri dependency (tools/genvectors).
  • Committed benchmark corpus at testdata/corpus/uri_bench.tsv with net/url comparison scaffolding, fuzz tests, and alloc tests.

Verification

  • go build ./... — clean
  • go vet ./... — clean
  • go test ./... — PASS

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

zchee added 8 commits June 11, 2026 12:06
Replace the legacy string wrapper with a canonical comparable URI value.
This keeps map keys aligned with the encoded transport form while
preserving zero runtime dependencies and pinned vscode-uri vectors.

Rejected: preserving parse-history casing in comparable URI fields.
Constraint: component accessors expose canonical reparse semantics.
Document the canonical-reparse contract, migration path, vector
regeneration workflow, and benchmark evidence so downstream users can
review the breaking rewrite without relying on hidden assumptions.
Preserve direct URI string conversions while keeping canonical
constructor behavior and the clean file/FsPath allocation gates.

Rejected: cached struct metadata; it breaks requested URI string representation.
Document component-specific escaping so callers know which constructor
outputs are safe for LSP wire text and where raw syntax is preserved.
@zchee
zchee merged commit 7d9d40a into main Jun 11, 2026
5 checks passed
@zchee
zchee deleted the fastest branch June 11, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant