From 489415d94e32de505a9fb5e0a0f7b84676b5e5f4 Mon Sep 17 00:00:00 2001 From: nileshpatil6 Date: Tue, 25 Aug 2026 21:07:42 +0530 Subject: [PATCH] measurement: count runes when stripping the plural suffix in sniffUnit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sniffUnit strips a trailing "s" only when the unit is longer than two characters, but len() counts bytes. The micro sign spelling registered as an alias of the microsecond unit, {"us", []string{"μs", "us", "microsecond"}, float64(time.Microsecond)}, is two runes and three bytes, so the guard fires and the alias is trimmed to "μ", which matches nothing. The alias has therefore never resolved. Three consequences, all silent: Scale(2000, "μs", "ms") -> (2000, "ms") no scaling applied pprof -unit=μs -> prints 0 unknown toUnit falls back to seconds merging a μs profile -> incompatible types: {cpu ms} {cpu μs} Count runes so a two rune alias is left alone. --- internal/measurement/measurement.go | 5 ++- internal/measurement/measurement_test.go | 44 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/internal/measurement/measurement.go b/internal/measurement/measurement.go index 479235c0a6..d70e8e6019 100644 --- a/internal/measurement/measurement.go +++ b/internal/measurement/measurement.go @@ -21,6 +21,7 @@ import ( "slices" "strings" "time" + "unicode/utf8" "github.com/google/pprof/profile" ) @@ -209,7 +210,9 @@ func (ut UnitType) findByAlias(alias string) *Unit { // specified alias. It returns nil if the unit with such alias is not found. func (ut UnitType) sniffUnit(unit string) *Unit { unit = strings.ToLower(unit) - if len(unit) > 2 { + // Count runes rather than bytes so that multi-byte aliases such as "μs" + // are not mistaken for a plural form and stripped down to "μ". + if utf8.RuneCountInString(unit) > 2 { unit = strings.TrimSuffix(unit, "s") } return ut.findByAlias(unit) diff --git a/internal/measurement/measurement_test.go b/internal/measurement/measurement_test.go index 7521a64d26..b75d3b5a85 100644 --- a/internal/measurement/measurement_test.go +++ b/internal/measurement/measurement_test.go @@ -17,6 +17,8 @@ package measurement import ( "math" "testing" + + "github.com/google/pprof/profile" ) func TestScale(t *testing.T) { @@ -37,6 +39,10 @@ func TestScale(t *testing.T) { {2048, "mb", "auto", 2, "GB"}, {3.1536e7, "s", "auto", 8760, "hrs"}, {-1, "s", "ms", -1000, "ms"}, + {1, "μs", "ms", 0.001, "ms"}, + {2000, "μs", "auto", 2, "ms"}, + {1, "μS", "ns", 1000, "ns"}, + {1, "us", "μs", 1, "us"}, {1, "foo", "count", 1, ""}, {1, "foo", "bar", 1, "bar"}, {2000, "count", "count", 2000, ""}, @@ -74,3 +80,41 @@ func floatEqual(a, b float64) bool { avg := (math.Abs(a) + math.Abs(b)) / 2 return diff/avg < 0.0001 } + +func TestCommonValueType(t *testing.T) { + for _, tc := range []struct { + desc string + in []*profile.ValueType + wantUnit string + wantErr bool + }{ + { + desc: "microseconds and milliseconds are compatible", + in: []*profile.ValueType{{Type: "cpu", Unit: "ms"}, {Type: "cpu", Unit: "μs"}}, + wantUnit: "μs", + }, + { + desc: "microseconds and seconds are compatible", + in: []*profile.ValueType{{Type: "cpu", Unit: "μs"}, {Type: "cpu", Unit: "s"}}, + wantUnit: "μs", + }, + { + desc: "time and memory units are incompatible", + in: []*profile.ValueType{{Type: "cpu", Unit: "μs"}, {Type: "cpu", Unit: "kb"}}, + wantErr: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + got, err := CommonValueType(tc.in) + if gotErr := err != nil; gotErr != tc.wantErr { + t.Fatalf("CommonValueType(%v) error = %v, want error presence %v", tc.in, err, tc.wantErr) + } + if tc.wantErr { + return + } + if got == nil || got.Unit != tc.wantUnit { + t.Errorf("CommonValueType(%v) = %v, want unit %q", tc.in, got, tc.wantUnit) + } + }) + } +}