Repository: lightstep/otel-launcher-go
Pull request #396 (2023-02-27 22:45:43)

Add an inactivity period; set to 10 collection periods by default
**Description:** 
Adds a performance setting `InactiveCollectionPeriods` that determines how many `Collect()` cycles the synchronous instrument state will retain records in memory, which makes `Update()` operations faster for less-frequently used attribute sets and instruments.

Replaces atomic 64-bit field with 32-bit field, it was unnecessary width--only need to avoid wraparound between collections. 

Removes unnecessary atomic operations on `collectionCount`, it's always modified under the instrument-level lock.

Adds new 32-bit record-level field, `inactiveCount`, also modified under the instrument-level lock.

Altogether, no increase in struct size.

**Link to tracking Issue:**  #377 

**Testing:** New test.

**Documentation:** Performance setting section updated.

codecov-commenter: # [Codecov](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep) Report
Base: **87.78**% // Head: **87.83**% // Increases project coverage by **`+0.04%`** :tada:
> Coverage data is based on head [(`55629ad`)](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep) compared to base [(`d6beb6a`)](https://codecov.io/gh/lightstep/otel-launcher-go/commit/d6beb6ac9b62c9b2e8837743ab017ebcb74a76fb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep).
> Patch coverage: 100.00% of modified lines in pull request are covered.

> :exclamation: Current head 55629ad differs from pull request most recent head 913c9b3. Consider uploading reports for the commit 913c9b3 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #396      +/-   ##
==========================================
+ Coverage   87.78%   87.83%   +0.04%     
==========================================
  Files          74       75       +1     
  Lines        4217     4232      +15     
==========================================
+ Hits         3702     3717      +15     
  Misses        441      441              
  Partials       74       74              
```


| [Impacted Files](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep) | Coverage Δ | |
|---|---|---|
| [lightstep/sdk/metric/internal/syncstate/sync.go](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep#diff-bGlnaHRzdGVwL3Nkay9tZXRyaWMvaW50ZXJuYWwvc3luY3N0YXRlL3N5bmMuZ28=) | `97.83% <100.00%> (+0.12%)` | :arrow_up: |
| [...p/sdk/metric/internal/viewstate/base\_instrument.go](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep#diff-bGlnaHRzdGVwL3Nkay9tZXRyaWMvaW50ZXJuYWwvdmlld3N0YXRlL2Jhc2VfaW5zdHJ1bWVudC5nbw==) | `100.00% <100.00%> (ø)` | |
| [...htstep/sdk/metric/internal/viewstate/collectors.go](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep#diff-bGlnaHRzdGVwL3Nkay9tZXRyaWMvaW50ZXJuYWwvdmlld3N0YXRlL2NvbGxlY3RvcnMuZ28=) | `100.00% <100.00%> (ø)` | |
| [lightstep/sdk/metric/provider.go](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep#diff-bGlnaHRzdGVwL3Nkay9tZXRyaWMvcHJvdmlkZXIuZ28=) | `100.00% <100.00%> (ø)` | |
| [lightstep/sdk/metric/sdkinstrument/performance.go](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep#diff-bGlnaHRzdGVwL3Nkay9tZXRyaWMvc2RraW5zdHJ1bWVudC9wZXJmb3JtYW5jZS5nbw==) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/lightstep/otel-launcher-go/pull/396?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lightstep).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d3af54..e3b55c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 ## Unreleased
 
+## [1.13.2](https://github.com/lightstep/otel-launcher-go/releases/tag/v1.13.2) - 2022-02-28)
+
+- Adds performance setting `InactiveCollectionPeriods` allowing
+  control over how fast records are removed from memory. [#396](https://github.com/lightstep/otel-launcher-go/pull/396)
+
 ## [1.13.1](https://github.com/lightstep/otel-launcher-go/releases/tag/v1.13.1) - 2022-02-15)
 
 - Adds performance improvements for concurrent use of synchronous
diff --git a/VERSION b/VERSION
index b50dd27..61ce01b 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.13.1
+1.13.2
diff --git a/go.mod b/go.mod
index ad0a7e4..0677872 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,8 @@ module github.com/lightstep/otel-launcher-go
 go 1.18
 
 require (
-	github.com/lightstep/otel-launcher-go/lightstep/sdk/metric v1.13.1
-	github.com/lightstep/otel-launcher-go/pipelines v1.13.1
+	github.com/lightstep/otel-launcher-go/lightstep/sdk/metric v1.13.2
+	github.com/lightstep/otel-launcher-go/pipelines v1.13.2
 	github.com/sethvargo/go-envconfig v0.8.3
 	github.com/stretchr/testify v1.8.1
 	go.opentelemetry.io/otel v1.12.0
@@ -23,7 +23,7 @@ require (
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
 	github.com/lightstep/go-expohisto v1.0.0 // indirect
-	github.com/lightstep/otel-launcher-go/lightstep/instrumentation v1.13.1 // indirect
+	github.com/lightstep/otel-launcher-go/lightstep/instrumentation v1.13.2 // indirect
 	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
diff --git a/launcher/version.go b/launcher/version.go
index 339118c..005edb3 100644
--- a/launcher/version.go
+++ b/launcher/version.go
@@ -14,4 +14,4 @@
 
 package launcher
 
-const version = "1.13.1"
+const version = "1.13.2"
diff --git a/lightstep/sdk/metric/README.md b/lightstep/sdk/metric/README.md
index 21eed7f..6ff71d9 100644
--- a/lightstep/sdk/metric/README.md
+++ b/lightstep/sdk/metric/README.md
@@ -134,3 +134,16 @@ With `IgnoreCollisions` set to true, the SDK will ignore fingerprint
 collisions and bypass a safety mechanism that ensures correctness in
 spite of fingerprint collisions in the fast path for synchronous
 instruments.
+
+#### InactiveCollectionPeriods
+
+The `InactiveCollectionPeriods` setting controls how many `Collect()`
+cycles with no updates are allowed before a record is removed from
+intermediate state.  Larger values indicate more memory will be used
+and that callers are less likely to block while creating new
+aggregators.
+
+Setting this field to 1 means records will be removed from memory
+after one inactive collection cycle.
+
+Setting this field to 0 causes the default value 10 to be used.
diff --git a/lightstep/sdk/metric/data/sequence.go b/lightstep/sdk/metric/data/sequence.go
index c0e2ad2..68f02b5 100644
--- a/lightstep/sdk/metric/data/sequence.go
+++ b/lightstep/sdk/metric/data/sequence.go
@@ -39,7 +39,7 @@ type Collector interface {
 	// snapshots into output.
 	Collect(sequence Sequence, output *[]Instrument)
 
-	// Size returns the number of entries held in memory.  Size()
+	// InMemorySize returns the number of entries held in memory.  InMemorySize()
 	// is meant to be called following Collect().
-	Size() int
+	InMemorySize() int
 }
diff --git a/lightstep/sdk/metric/example/go.mod b/lightstep/sdk/metric/example/go.mod
index b1447f7..f04e3cd 100644
--- a/lightstep/sdk/metric/example/go.mod
+++ b/lightstep/sdk/metric/example/go.mod
@@ -3,7 +3,7 @@ module github.com/lightstep/otel-launcher-go/lightstep/sdk/metric/example
 go 1.18
 
 require (
-	github.com/lightstep/otel-launcher-go/lightstep/sdk/metric v1.13.1
+	github.com/lightstep/otel-launcher-go/lightstep/sdk/metric v1.13.2
 	github.com/lightstep/otel-launcher-go/pipelines v1.8.0
 	go.opentelemetry.io/proto/otlp v0.19.0
 )
diff --git a/lightstep/sdk/metric/internal/syncstate/sync.go b/lightstep/sdk/metric/internal/syncstate/sync.go
index dcc12ac..bb4ed79 100644
--- a/lightstep/sdk/metric/internal/syncstate/sync.go
+++ b/lightstep/sdk/metric/internal/syncstate/sync.go
@@ -75,6 +75,8 @@ func New(desc sdkinstrument.Descriptor, performance sdkinstrument.Performance, _
 		// When no readers enable the instrument, no need for an instrument.
 		return nil
 	}
+	// validate so that 0 is replaced w/ a better default.
+	performance = performance.Validate()
 	return &Observer{
 		descriptor:  desc,
 		current:     map[uint64]*record{},
@@ -110,7 +112,7 @@ func (inst *Observer) SnapshotAndProcess() {
 		// linked list after filtering records that are no longer
 		// in use.
 		for rec := reclist; rec != nil; rec = rec.next {
-			if inst.singleSnapshotAndProcess(key, rec) {
+			if inst.collect(key, rec) {
 				if head == nil {
 					// The first time a record will be kept,
 					// it becomes the head and tail.
@@ -143,22 +145,32 @@ func (inst *Observer) SnapshotAndProcess() {
 	}
 }
 
-// singleSnapshotAndProcess
-func (inst *Observer) singleSnapshotAndProcess(fp uint64, rec *record) bool {
-	if rec.conditionalSnapshotAndProcess(false) {
+// collect collects the record.  When the record has been inactive for
+// the configured number of periods, it is removed from memory.
+func (inst *Observer) collect(fp uint64, rec *record) bool {
+	if rec.normalCollect() {
+		rec.inactiveCount = 0
+		return true
+	}
+
+	// Allow the record to remain in an inactive state for a number
+	// of collections.
+	rec.inactiveCount++
+
+	if rec.inactiveCount < inst.performance.InactiveCollectionPeriods {
 		return true
 	}
 
 	// Having no updates since last collection, try to unmap:
 	unmapped := rec.refMapped.tryUnmap()
 
-	// The first rec.conditionalSnapshotAndProcess
-	// returned false indicating no change, except:
-	// (a) it's now possible there was a race, the collector needs to
-	// see it.
-	// (b) if this is indeed the last reference, the collector needs the
-	// release signal.
-	_ = rec.conditionalSnapshotAndProcess(unmapped)
+	// normalCollect returned false indicating no change, except:
+	// (a) it's now possible there was a race, the collector needs to see it.
+	// (b) if this is indeed the last reference, the collector needs the release signal.
+	_ = rec.scavengeCollect(unmapped)
+
+	// reset inactivity in case of !unmapped.
+	rec.inactiveCount = 0
 
 	// When `unmapped` is true, any other goroutines are now
 	// trying to re-insert this entry in the map, they are busy
@@ -174,12 +186,20 @@ type record struct {
 	// uncollected.
 	refMapped refcountMapped
 
-	// updateCount is incremented on every Update.
-	updateCount int64
+	// updateCount is incremented on every Update.  This field
+	// uses atomic load/store operations.
+	updateCount uint32
 
 	// collectedCount is set to updateCount on collection,
-	// supports checking for no updates during a round.
-	collectedCount int64
+	// supports checking for no updates during a round.  This
+	// field is read/written under the Observer lock.
+	collectedCount uint32
+
+	// inactiveCount is incremented when the record is collected
+	// but has not been updated.  This is used to compare against
+	// the Performance InactiveCollectionPeriods setting.  This
+	// field is read/written under the Observer lock.
+	inactiveCount uint32
 
 	// inst allows referring to performance settings.
 	inst *Observer
@@ -220,17 +240,29 @@ type record struct {
 	attrsUnsafe attribute.Sortable
 }
 
-// conditionalSnapshotAndProcess checks whether the accumulator has been
-// modified since the last collection (by any reader), returns a
-// boolean indicating whether the record is active.  If active, calls
+// normalCollect equals conditionalCollect(false), is named
+// differently from scavengeCollect for profiling.
+func (rec *record) normalCollect() bool {
+	return rec.conditionalCollect(false)
+}
+
+// scavengeCollect equals conditionalCollect(false), is named
+// differently from normalCollect for profiling.
+func (rec *record) scavengeCollect(release bool) bool {
+	return rec.conditionalCollect(release)
+}
+
+// conditionalCollect checks whether the accumulator has been modified
+// since the last collection (by any reader), returns a boolean
+// indicating whether the record is active.  If modified, calls
 // SnapshotAndProcess on the associated accumulator and returns true.
 // If updates happened since the last collection (by any reader),
 // returns false.
-func (rec *record) conditionalSnapshotAndProcess(release bool) bool {
-	mods := atomic.LoadInt64(&rec.updateCount)
+func (rec *record) conditionalCollect(release bool) bool {
+	mods := atomic.LoadUint32(&rec.updateCount)
 
 	if !release {
-		if mods == atomic.LoadInt64(&rec.collectedCount) {
+		if mods == rec.collectedCount {
 			return false
 		}
 	}
@@ -238,7 +270,7 @@ func (rec *record) conditionalSnapshotAndProcess(release bool) bool {
 	rec.readAccumulator().SnapshotAndProcess(release)
 
 	// Updates happened in this interval, collect and continue.
-	atomic.StoreInt64(&rec.collectedCount, mods)
+	rec.collectedCount = mods
 	return true
 }
 
@@ -303,7 +335,7 @@ func Observe[N number.Any, Traits number.Traits[N]](_ context.Context, inst *Obs
 	rec.readAccumulator().(viewstate.Updater[N]).Update(num)
 
 	// Record was modified.
-	atomic.AddInt64(&rec.updateCount, 1)
+	atomic.AddUint32(&rec.updateCount, 1)
 }
 
 func fingerprintAttributes(attrs []attribute.KeyValue) uint64 {
@@ -430,11 +462,16 @@ func acquireRead(inst *Observer, fp uint64, attrs []attribute.KeyValue) *record
 func acquireUninitialized[N number.Any](inst *Observer, attrs []attribute.KeyValue) *record {
 	fp := fingerprintAttributes(attrs)
 
-	rec := acquireRead(inst, fp, attrs)
-	if rec != nil {
+	if rec := acquireRead(inst, fp, attrs); rec != nil {
 		return rec
 	}
 
+	return acquireNotfound[N](inst, fp, attrs)
+}
+
+// acquireNotfound is the code path taken when acquireRead does not
+// locate a record.
+func acquireNotfound[N number.Any](inst *Observer, fp uint64, attrs []attribute.KeyValue) *record {
 	newRec := &record{
 		inst:        inst,
 		refMapped:   newRefcountMapped(),
diff --git a/lightstep/sdk/metric/internal/viewstate/base_instrument.go b/lightstep/sdk/metric/internal/viewstate/base_instrument.go
index 8df9229..9590878 100644
--- a/lightstep/sdk/metric/internal/viewstate/base_instrument.go
+++ b/lightstep/sdk/metric/internal/viewstate/base_instrument.go
@@ -55,8 +55,8 @@ type instrumentBase[N number.Any, Storage, Auxiliary any, Methods aggregator.Met
 	keysFilter *attribute.Filter
 }
 
-// Size reports the size of the data map.
-func (metric *instrumentBase[N, Storage, Auxiliary, Methods]) Size() int {
+// InMemorySize reports the size of the data map.
+func (metric *instrumentBase[N, Storage, Auxiliary, Methods]) InMemorySize() int {
 	metric.instLock.Lock()
 	defer metric.instLock.Unlock()
 	return len(metric.data)
diff --git a/lightstep/sdk/metric/internal/viewstate/collectors.go b/lightstep/sdk/metric/internal/viewstate/collectors.go
index 6047daa..070822b 100644
--- a/lightstep/sdk/metric/internal/viewstate/collectors.go
+++ b/lightstep/sdk/metric/internal/viewstate/collectors.go
@@ -121,7 +121,7 @@ type statefulAsyncInstrument[N number.Any, Storage any, Methods aggregator.Metho
 
 // Size (special case) reports the size of the prior map, since
 // data is emptied on Collect().
-func (p *statefulAsyncInstrument[N, Storage, Methods]) Size() int {
+func (p *statefulAsyncInstrument[N, Storage, Methods]) InMemorySize() int {
 	p.instLock.Lock()
 	defer p.instLock.Unlock()
 	return len(p.prior)
diff --git a/lightstep/sdk/metric/provider.go b/lightstep/sdk/metric/provider.go
index 3482db8..d0f0c04 100644
--- a/lightstep/sdk/metric/provider.go
+++ b/lightstep/sdk/metric/provider.go
@@ -59,6 +59,7 @@ func NewMeterProvider(options ...Option) *MeterProvider {
 	for _, option := range options {
 		cfg = option.apply(cfg)
 	}
+	cfg.performance = cfg.performance.Validate()
 	p := &MeterProvider{
 		cfg:       cfg,
 		startTime: time.Now(),
diff --git a/lightstep/sdk/metric/sdkinstrument/performance.go b/lightstep/sdk/metric/sdkinstrument/performance.go
index 2d60daa..e37495c 100644
--- a/lightstep/sdk/metric/sdkinstrument/performance.go
+++ b/lightstep/sdk/metric/sdkinstrument/performance.go
@@ -14,6 +14,10 @@
 
 package sdkinstrument
 
+// DefaultInactiveCollectionPeriods is how many collection periods to
+// delay before removing records from memory.
+const DefaultInactiveCollectionPeriods = 10
+
 // Performace configures features that allow the user to control
 // performance.
 type Performance struct {
@@ -21,4 +25,21 @@ type Performance struct {
 	// attributes-set comparison after finding a fingerprint
 	// match.
 	IgnoreCollisions bool
+
+	// InactiveCollectionPeriods is the number of allowed
+	// collection periods having no updates before the record is
+	// removed from memory.
+	InactiveCollectionPeriods uint32
+}
+
+// Validate returns a Performance object with 0 values replaced by
+// defaults and errors checked.
+func (p Performance) Validate() Performance {
+	// If InactiveCollectionPeriods 0 is a valid setting, but can
+	// lead to poor performance, so we let it use the default. The
+	// user can configure 1 for the same effect as 0.
+	if p.InactiveCollectionPeriods == 0 {
+		p.InactiveCollectionPeriods = DefaultInactiveCollectionPeriods
+	}
+	return p
 }
diff --git a/pipelines/go.mod b/pipelines/go.mod
index c0703fd..5fe151e 100644
--- a/pipelines/go.mod
+++ b/pipelines/go.mod
@@ -51,8 +51,8 @@ require (
 )
 
 require (
-	github.com/lightstep/otel-launcher-go/lightstep/instrumentation v1.13.1
-	github.com/lightstep/otel-launcher-go/lightstep/sdk/metric v1.13.1
+	github.com/lightstep/otel-launcher-go/lightstep/instrumentation v1.13.2
+	github.com/lightstep/otel-launcher-go/lightstep/sdk/metric v1.13.2
 	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.35.0
 )
 


Test patch:
diff --git a/lightstep/sdk/metric/internal/syncstate/sync_test.go b/lightstep/sdk/metric/internal/syncstate/sync_test.go
index 108c220..acc9d95 100644
--- a/lightstep/sdk/metric/internal/syncstate/sync_test.go
+++ b/lightstep/sdk/metric/internal/syncstate/sync_test.go
@@ -17,6 +17,7 @@ package syncstate
 import (
 	"context"
 	"errors"
+	"fmt"
 	"math"
 	"math/rand"
 	"sync"
@@ -52,7 +53,8 @@ var (
 	}
 
 	safePerf = sdkinstrument.Performance{
-		IgnoreCollisions: false,
+		IgnoreCollisions:          false,
+		InactiveCollectionPeriods: 1,
 	}
 )
 
@@ -775,7 +777,7 @@ func TestDuplicateFingerprintSafety(t *testing.T) {
 	)
 
 	// There are 2 entries in memory
-	require.Equal(t, 2, vcs[0].Collectors()[0].Size())
+	require.Equal(t, 2, vcs[0].Collectors()[0].InMemorySize())
 
 	// collect reader 0 again
 	inst.SnapshotAndProcess()
@@ -792,7 +794,7 @@ func TestDuplicateFingerprintSafety(t *testing.T) {
 	)
 
 	// There are 0 entries in memory
-	require.Equal(t, 0, vcs[0].Collectors()[0].Size())
+	require.Equal(t, 0, vcs[0].Collectors()[0].InMemorySize())
 
 	// Use both again, collect reader 0 again
 	inst.ObserveFloat64(ctx, 5, attr1)
@@ -822,7 +824,7 @@ func TestDuplicateFingerprintSafety(t *testing.T) {
 	)
 
 	// There are 2 entries in memory again
-	require.Equal(t, 2, vcs[0].Collectors()[0].Size())
+	require.Equal(t, 2, vcs[0].Collectors()[0].InMemorySize())
 
 	// Update attr1, collect reader 0
 	inst.ObserveFloat64(ctx, 25, attr1)
@@ -846,7 +848,7 @@ func TestDuplicateFingerprintSafety(t *testing.T) {
 	)
 
 	// Only 1 entry in memory
-	require.Equal(t, 1, vcs[0].Collectors()[0].Size())
+	require.Equal(t, 1, vcs[0].Collectors()[0].InMemorySize())
 
 	// Update attr2, collect reader 0
 	inst.ObserveFloat64(ctx, 32, attr2)
@@ -870,7 +872,7 @@ func TestDuplicateFingerprintSafety(t *testing.T) {
 	)
 
 	// Only 1 entry in memory
-	require.Equal(t, 1, vcs[0].Collectors()[0].Size())
+	require.Equal(t, 1, vcs[0].Collectors()[0].InMemorySize())
 
 	// No updates, clear memory
 	inst.SnapshotAndProcess()
@@ -886,7 +888,7 @@ func TestDuplicateFingerprintSafety(t *testing.T) {
 		),
 	)
 
-	require.Equal(t, 0, vcs[0].Collectors()[0].Size())
+	require.Equal(t, 0, vcs[0].Collectors()[0].InMemorySize())
 
 	// collect reader 1
 	test.RequireEqualMetrics(
@@ -928,7 +930,8 @@ func TestDuplicateFingerprintCollisionIgnored(t *testing.T) {
 
 	inst := New(desc, sdkinstrument.Performance{
 		// Do not check the collision.
-		IgnoreCollisions: true,
+		IgnoreCollisions:          true,
+		InactiveCollectionPeriods: 1,
 	}, nil, pipes)
 	require.NotNil(t, inst)
 
@@ -959,7 +962,7 @@ func TestDuplicateFingerprintCollisionIgnored(t *testing.T) {
 	)
 
 	// There is 1 entry in memory
-	require.Equal(t, 1, vcs[0].Collectors()[0].Size())
+	require.Equal(t, 1, vcs[0].Collectors()[0].InMemorySize())
 
 	// collect reader again
 	inst.SnapshotAndProcess()
@@ -976,7 +979,7 @@ func TestDuplicateFingerprintCollisionIgnored(t *testing.T) {
 	)
 
 	// There are 0 entries in memory
-	require.Equal(t, 0, vcs[0].Collectors()[0].Size())
+	require.Equal(t, 0, vcs[0].Collectors()[0].InMemorySize())
 
 	// Use both attribute sets in the opposite order, collect
 	// reader again.
@@ -1001,3 +1004,111 @@ func TestDuplicateFingerprintCollisionIgnored(t *testing.T) {
 		),
 	)
 }
+
+func TestRecordInactivity(t *testing.T) {
+	for inactive := uint32(1); inactive < 10; inactive++ {
+		t.Run(fmt.Sprint(inactive), func(t *testing.T) {
+			ctx := context.Background()
+			lib := instrumentation.Library{
+				Name: "testlib",
+			}
+			vcs := make([]*viewstate.Compiler, 1)
+			vcs[0] = viewstate.New(lib, view.New(
+				"test",
+				deltaSelector,
+			))
+
+			desc := test.Descriptor("c", sdkinstrument.SyncCounter, number.Float64Kind)
+
+			pipes := make(pipeline.Register[viewstate.Instrument], 1)
+			pipes[0], _ = vcs[0].Compile(desc)
+
+			require.NotNil(t, pipes[0])
+
+			inst := New(desc, sdkinstrument.Performance{
+				InactiveCollectionPeriods: inactive,
+			}, nil, pipes)
+			require.NotNil(t, inst)
+
+			attr := attribute.Int64(fpKey, fpInt1)
+
+			expectNothing := func() {
+				test.RequireEqualMetrics(
+					t,
+					test.CollectScope(
+						t,
+						vcs[0].Collectors(),
+						testSequence,
+					),
+					test.Instrument(
+						desc,
+					),
+				)
+			}
+			expectSomething := func(value float64) {
+				test.RequireEqualMetrics(
+					t,
+					test.CollectScope(
+						t,
+						vcs[0].Collectors(),
+						testSequence,
+					),
+					test.Instrument(
+						desc,
+						test.Point(middleTime, endTime,
+							sum.NewMonotonicFloat64(value),
+							aggregation.DeltaTemporality,
+							attr,
+						),
+					),
+				)
+			}
+
+			inst.ObserveFloat64(ctx, 17, attr)
+
+			// collect reader
+			inst.SnapshotAndProcess()
+			expectSomething(17)
+
+			// There is 1 entry in memory
+			require.Equal(t, 1, vcs[0].Collectors()[0].InMemorySize())
+
+			repeatedlyNothing := func() {
+				// For 1 less than the inactive allowance,
+				// expect nothing collected and one record
+				// remaining in memory.
+				for i := uint32(1); i < inactive; i++ {
+					// collect reader again.
+					inst.SnapshotAndProcess()
+					expectNothing()
+
+					// There is still 1 entry in memory.
+					require.Equal(t, 1, vcs[0].Collectors()[0].InMemorySize())
+				}
+			}
+
+			// Inactive for the up to the allowed inactivity.
+			repeatedlyNothing()
+
+			inst.ObserveFloat64(ctx, 23, attr)
+
+			// collect reader again, but an update came in.
+			inst.SnapshotAndProcess()
+			expectSomething(23)
+
+			// There is still 1 entry in memory, the count has reset.
+			require.Equal(t, 1, vcs[0].Collectors()[0].InMemorySize())
+
+			// Inactive for the up to the allowed inactivity.
+			repeatedlyNothing()
+
+			// collect reader again
+			inst.SnapshotAndProcess()
+			expectNothing()
+
+			// There are now 0 entries in memory.
+			require.Equal(t, 0, vcs[0].Collectors()[0].InMemorySize())
+
+		})
+	}
+}
diff --git a/lightstep/sdk/metric/internal/viewstate/viewstate_test.go b/lightstep/sdk/metric/internal/viewstate/viewstate_test.go
index 1c01ce8..50ae135 100644
--- a/lightstep/sdk/metric/internal/viewstate/viewstate_test.go
+++ b/lightstep/sdk/metric/internal/viewstate/viewstate_test.go
@@ -959,20 +959,20 @@ func TestDeltaTemporalityAsyncCounter(t *testing.T) {
 	observe(10)
 	expectValues(10, seq)
 	tick()
-	require.Equal(t, 1, instF.(data.Collector).Size())
+	require.Equal(t, 1, instF.(data.Collector).InMemorySize())
 
 	expectNone(seq)
 	tick()
-	require.Equal(t, 0, instF.(data.Collector).Size())
+	require.Equal(t, 0, instF.(data.Collector).InMemorySize())
 
 	expectNone(seq)
 	tick()
-	require.Equal(t, 0, instF.(data.Collector).Size())
+	require.Equal(t, 0, instF.(data.Collector).InMemorySize())
 
 	observe(11)
 	expectValues(11, seq)
 	tick()
-	require.Equal(t, 1, instF.(data.Collector).Size())
+	require.Equal(t, 1, instF.(data.Collector).InMemorySize())
 
 	// No change here:
 	observe(11)
@@ -984,7 +984,7 @@ func TestDeltaTemporalityAsyncCounter(t *testing.T) {
 	tick()
 	// There is still a point mapped, even though it didn't output
 	// a value.
-	require.Equal(t, 1, instF.(data.Collector).Size())
+	require.Equal(t, 1, instF.(data.Collector).InMemorySize())
 
 	expectNone(seq)
 	tick()
@@ -994,13 +994,13 @@ func TestDeltaTemporalityAsyncCounter(t *testing.T) {
 	// keep track of the last value, otherwise it will reappear as
 	// new (and impact any rate calculations on the data).  Same
 	// for the two cases above.
-	require.Equal(t, 0, instF.(data.Collector).Size())
+	require.Equal(t, 0, instF.(data.Collector).InMemorySize())
 
 	observe(11)
 	expectValues(11, seq)
 	tick()
 
-	require.Equal(t, 1, instF.(data.Collector).Size())
+	require.Equal(t, 1, instF.(data.Collector).InMemorySize())
 }
 
 // TestDeltaTemporalityAsyncGauge ensures that the asynchronous gauge
@@ -1178,7 +1178,7 @@ func TestDeltaTemporalitySyncGauge(t *testing.T) {
 	instSize := func(inst Instrument) int {
 		// This only works for single-view configurations.  A
 		// multiInstrument will not satisfy this type assertion.
-		return inst.(data.Collector).Size()
+		return inst.(data.Collector).InMemorySize()
 	}
 
 	// start with one observation, collect