internal/span: optimise URI.Filename to avoid allocation

This change adds a fast-path check for the common case:
"file:", lowercase, followed by a simple POSIX absolute
file name without special characters.

This function used to account for 1% of CPU on the DidChange
benchmark (and I'm sure I've seen higher fractions on other
tests--but perhaps that was before the clone optimizations?).

It was tested by adding an assertion that it agrees with the
slow path and running all our tests.

Change-Id: I15492b8a317715468870b00041bf8f6b0bb53bb2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/411900
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/internal/span/uri.go b/internal/span/uri.go
index f2b39ca..8132665 100644
--- a/internal/span/uri.go
+++ b/internal/span/uri.go
@@ -35,13 +35,29 @@
 }
 
 func filename(uri URI) (string, error) {
-	// This function is frequently called and its cost is
-	// dominated by the allocation of a net.URL.
-	// TODO(adonovan): opt: replace by a bespoke parseFileURI
-	// function that doesn't allocate.
 	if uri == "" {
 		return "", nil
 	}
+
+	// This conservative check for the common case
+	// of a simple non-empty absolute POSIX filename
+	// avoids the allocation of a net.URL.
+	if strings.HasPrefix(string(uri), "file:///") {
+		rest := string(uri)[len("file://"):] // leave one slash
+		for i := 0; i < len(rest); i++ {
+			b := rest[i]
+			// Reject these cases:
+			if b < ' ' || b == 0x7f || // control character
+				b == '%' || b == '+' || // URI escape
+				b == ':' || // Windows drive letter
+				b == '@' || b == '&' || b == '?' { // authority or query
+				goto slow
+			}
+		}
+		return rest, nil
+	}
+slow:
+
 	u, err := url.ParseRequestURI(string(uri))
 	if err != nil {
 		return "", err
@@ -54,6 +70,7 @@
 	if isWindowsDriveURIPath(u.Path) {
 		u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:]
 	}
+
 	return u.Path, nil
 }