mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 12:16:44 +02:00
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.
57 lines
1.8 KiB
Go
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
|
|
})
|