summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBrad Fitzpatrick <bradfitz@tailscale.com>2026-04-22 04:38:55 +0000
committerBrad Fitzpatrick <brad@danga.com>2026-04-24 09:48:11 -0700
commit006d7e180e4a46112b1905f1e2c2e1060ef31d8b (patch)
treed88b072d13265967c9d9f46012a450c45f1a883f
parent306fab796cd9d071b412a751cf002890a32788bb (diff)
downloadtailscale-006d7e180e4a46112b1905f1e2c2e1060ef31d8b.tar.xz
tailscale-006d7e180e4a46112b1905f1e2c2e1060ef31d8b.zip
version: use debug.ReadBuildInfo in CmdName on non-Windows
CmdName was re-opening the running executable and scanning it in 64KiB chunks for the Go modinfo markers on every call. The same modinfo is already parsed at startup and exposed via runtime/debug.ReadBuildInfo, so prefer that on non-Windows. Windows still takes the scanning path because its GUI-binary override keys off the on-disk executable name. benchstat of BenchmarkCmdName (Linux, before vs after): goos: linux goarch: amd64 pkg: tailscale.com/version cpu: Intel(R) Xeon(R) 6975P-C │ /tmp/old.txt │ /tmp/new.txt │ │ sec/op │ sec/op vs base │ CmdName-16 556045.5n ± 1% 825.6n ± 1% -99.85% (p=0.000 n=10) │ /tmp/old.txt │ /tmp/new.txt │ │ B/op │ B/op vs base │ CmdName-16 64.587Ki ± 0% 1.156Ki ± 0% -98.21% (p=0.000 n=10) │ /tmp/old.txt │ /tmp/new.txt │ │ allocs/op │ allocs/op vs base │ CmdName-16 8.000 ± 0% 7.000 ± 0% -12.50% (p=0.000 n=10) Fixes #19486 Change-Id: I925c5e28b64815a602459beb6c8dab8779339a6c Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
-rw-r--r--version/cmdname.go10
-rw-r--r--version/version_test.go20
2 files changed, 30 insertions, 0 deletions
diff --git a/version/cmdname.go b/version/cmdname.go
index 5a0b84875..8e6adb047 100644
--- a/version/cmdname.go
+++ b/version/cmdname.go
@@ -13,6 +13,7 @@ import (
"os"
"path"
"runtime"
+ "runtime/debug"
"strings"
)
@@ -20,6 +21,15 @@ import (
// using os.Executable. If os.Executable fails (it shouldn't), then
// "cmd" is returned.
func CmdName() string {
+ // On non-Windows, the modinfo embedded in the running binary is
+ // authoritative and avoids re-reading the executable from disk.
+ // Windows needs the executable-name-based GUI override in cmdName,
+ // so it still takes the slower path.
+ if runtime.GOOS != "windows" {
+ if info, ok := debug.ReadBuildInfo(); ok && info.Path != "" {
+ return path.Base(info.Path)
+ }
+ }
e, err := os.Executable()
if err != nil {
return "cmd"
diff --git a/version/version_test.go b/version/version_test.go
index 42bcf2163..01fcd47ec 100644
--- a/version/version_test.go
+++ b/version/version_test.go
@@ -6,6 +6,8 @@ package version_test
import (
"bytes"
"os"
+ "path"
+ "runtime/debug"
"testing"
ts "tailscale.com"
@@ -49,3 +51,21 @@ func TestShortAllocs(t *testing.T) {
t.Errorf("allocs = %v; want 0", allocs)
}
}
+
+func BenchmarkCmdName(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ _ = version.CmdName()
+ }
+}
+
+func BenchmarkReadBuildInfo(b *testing.B) {
+ b.ReportAllocs()
+ for b.Loop() {
+ info, ok := debug.ReadBuildInfo()
+ if !ok {
+ b.Fatal("ReadBuildInfo failed")
+ }
+ _ = path.Base(info.Path)
+ }
+}