tailscale/version/cmdname.go
Ubuntu 927ad0aef4 version: compute CmdName from in-memory build info, not on-disk scan
CmdName previously opened the running executable and streamed through
tens of MB searching for two 16-byte magic needles to find the Go
module info blob embedded by the linker, allocating a 64 KiB buffer on
every call. It was called at least twice during tailscaled startup
(logpolicy.LogsDir and logpolicy.Options.init on Windows) and once
more for tsweb's debug page title -- each call ~2.6 ms on a ~40 MB
tailscaled binary, and ~74 KB of allocator churn per call.

The same module info is exposed via runtime/debug.ReadBuildInfo, which
reads an already-resident string maintained by the Go runtime -- no
filesystem I/O. Use that, cached with sync.OnceValue so the lookup
happens at most once per process.

Benchmarks on Xeon 6975P-C (Linux amd64), against the version test
binary:

  BenchmarkCmdName before:  540,094 ns/op   66,136 B/op   8 allocs/op
  BenchmarkCmdName after:       2.5 ns/op        0 B/op   0 allocs/op

First-call (uncached) cost at tailscaled scale (71 deps in embedded
build info): 160 mallocs, ~11.5 KiB allocated, sub-microsecond. That
is smaller than the single 64 KiB scratch buffer the old code
allocated per call, and is now paid exactly once per process.

Stripped tailscaled binary size is unchanged: runtime/debug was
already imported transitively (including by sibling code in this same
package at version/version.go:121), so no new dependencies ship. The
hand-rolled byte scanner, 64 KiB scratch buffer, rsc.io/goversion-
derived magic constants, and the 32-second integration test that
built tailscaled to re-parse it are removed.

TestCmdNameNoAllocs asserts that, once primed, CmdName is 0 allocs
per call, guarding against regressions that reintroduce per-call
binary parsing. TestCmdNameFromBuildInfo verifies that CmdName is
pulling from the embedded build info rather than falling back to the
os.Executable basename.
2026-04-22 01:35:51 +00:00

57 lines
1.8 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package version
import (
"os"
"path"
"runtime"
"runtime/debug"
"strings"
"sync"
)
// CmdName returns either the base name of the current binary
// using os.Executable. If os.Executable fails (it shouldn't), then
// "cmd" is returned.
//
// The result is computed once per process and cached. It is recovered
// from the Go module info embedded in the running binary via
// [runtime/debug.ReadBuildInfo], which reads an already-resident
// string maintained by the runtime; no filesystem I/O is performed.
// This is materially cheaper than inferring the command name from
// the on-disk executable, which was previously done by scanning the
// entire binary for magic bytes on every call. CmdName is called at
// least twice during tailscaled startup on Windows (by logpolicy).
func CmdName() string { return cmdNameCached() }
var cmdNameCached = sync.OnceValue(func() string {
// fallbackName is derived from os.Executable and used if we cannot
// recover a package path from the binary's embedded build info.
var fallbackName string
if e, err := os.Executable(); err == nil {
fallbackName = prepExeNameForCmp(e, runtime.GOARCH)
} else {
fallbackName = "cmd"
}
bi, ok := debug.ReadBuildInfo()
if !ok || bi.Path == "" {
return fallbackName
}
// bi.Path is the main package import path, e.g.
// "tailscale.com/cmd/tailscaled". Go import paths are always
// forward-slash separated, so use path.Base, not filepath.Base.
ret := path.Base(bi.Path)
if runtime.GOOS == "windows" && strings.HasPrefix(ret, "gui") && checkPreppedExeNameForGUI(fallbackName) {
// The GUI binary, for internal build-system packaging reasons,
// has a path of "tailscale.io/win/gui". Ignore that name and
// use fallbackName instead.
return fallbackName
}
return ret
})