diff --git a/go/pkg/basecamp/eventfeed/connector.go b/go/pkg/basecamp/eventfeed/connector.go index f03720906..892f0821b 100644 --- a/go/pkg/basecamp/eventfeed/connector.go +++ b/go/pkg/basecamp/eventfeed/connector.go @@ -34,9 +34,11 @@ const ( // iteration cannot decline. The live buffer grows to its capacity lazily // and pays only for the events it admits; it carries the same ceiling // because the two are one published contract, not because it allocates up - // front. #900 refused the value at fixture load, after a scenario asking - // for 2,147,483,647 took the test process down. The options are the same - // request on a path the loader does not cover. + // front. That New itself spends nothing on either capacity is proven + // rather than asserted here: TestNewAllocationSizeDoesNotVaryWithCapacity + // measures it. #900 refused the value at fixture load, after a scenario + // asking for 2,147,483,647 took the test process down. The options are + // the same request on a path the loader does not cover. MaxCapacity = 1_000_000 // handshakeDeadline (EVENT_FEED_HANDSHAKE_DEADLINE, 10s) spans diff --git a/go/pkg/basecamp/eventfeed/connector_test.go b/go/pkg/basecamp/eventfeed/connector_test.go index 9e6301da5..0dee10ece 100644 --- a/go/pkg/basecamp/eventfeed/connector_test.go +++ b/go/pkg/basecamp/eventfeed/connector_test.go @@ -6,6 +6,7 @@ package eventfeed_test import ( "context" "errors" + "math" "runtime" "strings" "sync" @@ -167,6 +168,80 @@ func TestNewAcceptsCapacitiesAtTheCeiling(t *testing.T) { } } +// TestNewAllocationSizeDoesNotVaryWithCapacity is the MaxCapacity comment's +// cost claim, written as an assertion. That comment justifies the ceiling by +// naming where a configured capacity is actually spent — newDedupe sizes its +// index map by it eagerly, on the first Events iteration — and that account +// holds only while New itself spends nothing on it. An earlier wording said +// both capacities were allocated the moment the connector was constructed; +// nothing in the suite disagreed, and a reader caught it instead. +// +// Bytes rather than testing.AllocsPerRun's count, because the count cannot +// carry this claim: sizing anything by the capacity — make([]byte, capacity) +// in New — is ONE allocation whether the capacity is 1 or a million, so the +// count is identical in exactly the case that must fail. +// +// The assertion is the invariance, not the size: every configuration is +// compared against the smallest one, so an unrelated allocation added to New +// moves them all together and stays green while a New that began sizing +// anything by a capacity does not. The two capacities move independently, +// including in both orderings, so an allocation taken from their relationship +// rather than from either alone is caught too. +func TestNewAllocationSizeDoesNotVaryWithCapacity(t *testing.T) { + smallest := bytesAllocatedByNew(t, 1, 1) + if smallest == 0 { + t.Fatal("New allocated no measurable bytes: the construction was optimized away, so this test can no longer observe it") + } + + for _, tc := range []struct { + name string + dedupe, liveBuffer int + }{ + {"both capacities at the ceiling", eventfeed.MaxCapacity, eventfeed.MaxCapacity}, + {"dedupe capacity alone at the ceiling", eventfeed.MaxCapacity, 1}, + {"live buffer capacity alone at the ceiling", 1, eventfeed.MaxCapacity}, + } { + t.Run(tc.name, func(t *testing.T) { + got := bytesAllocatedByNew(t, tc.dedupe, tc.liveBuffer) + if got != smallest { + t.Fatalf("New allocated %d bytes at dedupe %d / live buffer %d and %d at 1/1: construction now sizes something by a capacity, which only the run is supposed to do", got, tc.dedupe, tc.liveBuffer, smallest) + } + }) + } +} + +// newSink keeps each constructed connector reachable, so the allocation being +// measured cannot be optimized away. +var newSink *eventfeed.Connector + +// bytesAllocatedByNew measures one New at one pair of capacities. TotalAlloc +// is a process-wide counter and this package runs connectors on their own +// goroutines throughout its suite, so an unrelated allocation landing between +// the two reads can only ADD to a measurement. The lowest of several is +// therefore the floor, and the floors are what the configurations are compared +// at. +func bytesAllocatedByNew(t *testing.T, dedupe, liveBuffer int) uint64 { + t.Helper() + + minter := feedtest.NewMinter() + polls := feedtest.NewPolls() + lowest := uint64(math.MaxUint64) + for range 5 { + var before, after runtime.MemStats + runtime.ReadMemStats(&before) + c, err := eventfeed.New(testOrigin, "1", minter, polls, + eventfeed.WithDedupeCapacity(dedupe), + eventfeed.WithLiveBufferCapacity(liveBuffer)) + runtime.ReadMemStats(&after) + if err != nil { + t.Fatalf("New at dedupe %d / live buffer %d: %v", dedupe, liveBuffer, err) + } + newSink = c + lowest = min(lowest, after.TotalAlloc-before.TotalAlloc) + } + return lowest +} + func TestNewValidConfigurations(t *testing.T) { minter := feedtest.NewMinter() polls := feedtest.NewPolls()