mock: avoid copying call history in AssertNumberOfCalls
AssertNumberOfCalls currently calls m.calls(), which copies the entire call history before counting entries with a matching method name. The method already holds m.mutex throughout the loop, and the loop only reads call.Method.
The copy originally served a purpose. In b11fb16, calls() acquired the mutex, copied the slice, and released the mutex before the caller iterated over it. Later, bd79c01 moved locking into AssertNumberOfCalls and removed the lock inside calls(), but kept the copy.
Could this particular loop iterate over m.Calls directly?
- for _, call := range m.calls() {
+ for _, call := range m.Calls {I benchmarked this against commit 435c07b5, repeatedly checking 100 recorded calls with a minimal test reporter. On Ubuntu 24.04 ARM64 in Lima, using Go 1.27.1, the median of 10 runs changed as follows:
- Time per assertion: 3.60 µs to 0.447 µs.
- Bytes allocated per assertion: 18,564 to 128.
- Allocations per assertion: 4 to 3.
This measures the assertion itself. An empty history showed no clear improvement: about 177 ns before and 179 ns after on recheck, with unchanged allocations.
The lock, counting logic, and failure message would remain unchanged. The local prototype passed the full test suite and race checks on macOS and Linux. This proposal only covers AssertNumberOfCalls, leaving calls() and its other callers unchanged.
Source: stretchr/testify