Skip to content

measurement: count runes when stripping the plural suffix in sniffUnit - #1019

Merged
aalexand merged 2 commits into
google:mainfrom
nileshpatil6:fix/sniff-unit-rune-count
Aug 25, 2026
Merged

measurement: count runes when stripping the plural suffix in sniffUnit#1019
aalexand merged 2 commits into
google:mainfrom
nileshpatil6:fix/sniff-unit-rune-count

Conversation

@nileshpatil6

Copy link
Copy Markdown
Contributor

The bug

sniffUnit strips a trailing "s" only when the unit is longer than two characters:

func (ut UnitType) sniffUnit(unit string) *Unit {
	unit = strings.ToLower(unit)
	if len(unit) > 2 {
		unit = strings.TrimSuffix(unit, "s")
	}
	return ut.findByAlias(unit)
}

len() on a string counts bytes. The micro sign spelling is deliberately registered as an alias of the microsecond unit:

{"us", []string{"μs", "us", "microsecond"}, float64(time.Microsecond)},

"μs" is two runes but three bytes, because μ is U+03BC (0xCE 0xBC). So the guard fires, the alias is trimmed to "μ", and findByAlias matches nothing. That alias has never resolved.

Why it matters

sniffUnit is the entry point for every unit lookup, so the failure shows up three ways, all silent:

Wrong numbers from Scale. With no UnitType claiming "μs", Scale falls through to the non-interesting-unit path and returns the value unchanged while still attaching the target unit's name:

Scale(2000, "μs", "ms")  ->  (2000, "ms")     // 1000x off, no error

-unit=μs prints zero. cfg.Unit reaches measurement.Scale(v, o.SampleUnit, o.OutputUnit) in internal/report/report.go. An unrecognized toUnit falls back to the time type's default of seconds, so a 2.5 ms sample scales to 0.0025 and ScaledLabel truncates it:

$ pprof -top -unit=us  ...     2500us   100%   main.work
$ pprof -top -unit=μs  ...          0   100%   main.work

Profile merging fails. compatibleValueTypes asks whether both units sniff to the same UnitType. Since "μs" sniffs to nothing:

incompatible types: {cpu ms} {cpu μs}

The change

Count runes instead of bytes, so a two-rune alias is left alone.

I considered simply dropping the "μs" alias instead, but the rune count is the more faithful reading: the alias was clearly written on purpose, and counting runes also fixes the toUnit direction and compatibleValueTypes, which removing the alias would not.

Tests

Four cases added to the existing TestScale table, plus a new TestCommonValueType covering the merge path. Without the change:

measurement_test.go:72: Scale(1, "μs", "ms") = (1, "ms"), want (0.001, "ms")
measurement_test.go:72: Scale(2000, "μs", "auto") = (2000, ""), want (2, "ms")
measurement_test.go:72: Scale(1, "μS", "ns") = (1, "ns"), want (1000, "ns")
measurement_test.go:72: Scale(1, "us", "μs") = (1e-06, "s"), want (1, "us")
CommonValueType(...) error = incompatible types: {cpu ms} {cpu μs}, want error presence false

With it, the package passes. gofmt -l internal/measurement/ and go vet ./internal/measurement/ are clean.

Full go test ./... matches a clean checkout: the only failures are pre-existing and environmental on my machine (internal/binutils TestPEFile and internal/report TestPrintAssemblyErrorMessage, both because nm/objdump are not on PATH here). No new failures.

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.
@aalexand
aalexand enabled auto-merge (squash) August 25, 2026 17:10
@codecov-commenter

codecov-commenter commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 67.38%. Comparing base (adde81e) to head (9d5f86f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1019      +/-   ##
==========================================
+ Coverage   67.20%   67.38%   +0.17%     
==========================================
  Files          45       45              
  Lines        7846     7846              
==========================================
+ Hits         5273     5287      +14     
+ Misses       2133     2115      -18     
- Partials      440      444       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aalexand
aalexand merged commit 4d45320 into google:main Aug 25, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants