Repository: uber-go/dig
Pull request #412 (2024-06-27 19:54:09)

feat: add function runtime to dig.CallbackInfo
This change adds runtime of the associated constructor 
or decorator to `dig.CallbackInfo`.

For example, users can access the runtime of a particular
constructor by:
```go
c := dig.New()
c.Provide(NewFoo, dig.WithProviderCallback(func(ci dig.CallbackInfo) {
    if ci.Error == nil {
        fmt.Printf("constructor %q finished running in %v", ci.Name, ci.Runtime)
    }
}))
```

This change is a prerequisite for adding https://github.com/uber-go/fx/issues/1213 
to report runtime of constructors in Run events.



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/uber-go/dig/pull/412?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
Attention: Patch coverage is `95.23810%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 98.35%. Comparing base [(`897df36`)](https://app.codecov.io/gh/uber-go/dig/commit/897df361fb2ddf798d605f6ef0ba4ae3737e241a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) to head [(`1ed1d62`)](https://app.codecov.io/gh/uber-go/dig/commit/1ed1d628239402943b262a60364a0cf6195416b8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).

> :exclamation: **Current head 1ed1d62 differs from pull request most recent head 7e6b2ed**
> 
> Please [upload](https://docs.codecov.com/docs/codecov-uploader) reports for the commit 7e6b2ed to get more accurate results.

| [Files](https://app.codecov.io/gh/uber-go/dig/pull/412?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Patch % | Lines |
|---|---|---|
| [container.go](https://app.codecov.io/gh/uber-go/dig/pull/412?src=pr&el=tree&filepath=container.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-Y29udGFpbmVyLmdv) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/uber-go/dig/pull/412?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) |

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


```diff
@@            Coverage Diff             @@
##           master     #412      +/-   ##
==========================================
- Coverage   98.40%   98.35%   -0.05%     
==========================================
  Files          22       23       +1     
  Lines        1507     1524      +17     
==========================================
+ Hits         1483     1499      +16     
- Misses         15       16       +1     
  Partials        9        9              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/uber-go/dig/pull/412?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).


Patch:
diff --git a/callback.go b/callback.go
index dfe47ea..ba0c36b 100644
--- a/callback.go
+++ b/callback.go
@@ -20,6 +20,8 @@
 
 package dig
 
+import "time"
+
 // CallbackInfo contains information about a provided function or decorator
 // called by Dig, and is passed to a [Callback] registered with
 // [WithProviderCallback] or [WithDecoratorCallback].
@@ -32,6 +34,10 @@ type CallbackInfo struct {
 	// function, if any. When used in conjunction with [RecoverFromPanics],
 	// this will be set to a [PanicError] when the function panics.
 	Error error
+
+	// Runtime contains the duration it took for the associated
+	// function to run.
+	Runtime time.Duration
 }
 
 // Callback is a function that can be registered with a provided function
diff --git a/constructor.go b/constructor.go
index 034c41c..adec5fd 100644
--- a/constructor.go
+++ b/constructor.go
@@ -161,11 +161,13 @@ func (n *constructorNode) Call(c containerStore) (err error) {
 	}
 
 	if n.callback != nil {
+		start := c.clock().Now()
 		// Wrap in separate func to include PanicErrors
 		defer func() {
 			n.callback(CallbackInfo{
-				Name:  fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
-				Error: err,
+				Name:    fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
+				Error:   err,
+				Runtime: c.clock().Since(start),
 			})
 		}()
 	}
diff --git a/container.go b/container.go
index 983fd3f..a875b5e 100644
--- a/container.go
+++ b/container.go
@@ -25,6 +25,7 @@ import (
 	"math/rand"
 	"reflect"
 
+	"go.uber.org/dig/internal/digclock"
 	"go.uber.org/dig/internal/dot"
 )
 
@@ -141,6 +142,9 @@ type containerStore interface {
 
 	// Returns invokerFn function to use when calling arguments.
 	invoker() invokerFn
+
+	// Returns a clock to use
+	clock() digclock.Clock
 }
 
 // New constructs a Container.
@@ -211,6 +215,21 @@ func (o setRandOption) applyOption(c *Container) {
 	c.scope.rand = o.r
 }
 
+// Changes the source of time for the container.
+func setClock(c digclock.Clock) Option {
+	return setClockOption{c: c}
+}
+
+type setClockOption struct{ c digclock.Clock }
+
+func (o setClockOption) String() string {
+	return fmt.Sprintf("setClock(%v)", o.c)
+}
+
+func (o setClockOption) applyOption(c *Container) {
+	c.scope.clockSrc = o.c
+}
+
 // DryRun is an Option which, when set to true, disables invocation of functions supplied to
 // Provide and Invoke. Use this to build no-op containers.
 func DryRun(dry bool) Option {
diff --git a/decorate.go b/decorate.go
index df362e9..f4c6be1 100644
--- a/decorate.go
+++ b/decorate.go
@@ -122,11 +122,13 @@ func (n *decoratorNode) Call(s containerStore) (err error) {
 	}
 
 	if n.callback != nil {
+		start := s.clock().Now()
 		// Wrap in separate func to include PanicErrors
 		defer func() {
 			n.callback(CallbackInfo{
-				Name:  fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
-				Error: err,
+				Name:    fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
+				Error:   err,
+				Runtime: s.clock().Since(start),
 			})
 		}()
 	}
diff --git a/internal/digclock/clock.go b/internal/digclock/clock.go
new file mode 100644
index 0000000..e38c55c
--- /dev/null
+++ b/internal/digclock/clock.go
@@ -0,0 +1,82 @@
+// Copyright (c) 2024 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package digclock
+
+import (
+	"time"
+)
+
+// Clock defines how dig accesses time.
+type Clock interface {
+	Now() time.Time
+	Since(time.Time) time.Duration
+}
+
+// System is the default implementation of Clock based on real time.
+var System Clock = systemClock{}
+
+type systemClock struct{}
+
+func (systemClock) Now() time.Time {
+	return time.Now()
+}
+
+func (systemClock) Since(t time.Time) time.Duration {
+	return time.Since(t)
+}
+
+// Mock is a fake source of time.
+// It implements standard time operations, but allows
+// the user to control the passage of time.
+//
+// Use the [Mock.Add] method to progress time.
+//
+// Note that this implementation is not safe for concurrent use.
+type Mock struct {
+	now time.Time
+}
+
+var _ Clock = (*Mock)(nil)
+
+// NewMock creates a new mock clock with the current time set to the current time.
+func NewMock() *Mock {
+	return &Mock{now: time.Now()}
+}
+
+// Now returns the current time.
+func (m *Mock) Now() time.Time {
+	return m.now
+}
+
+// Since returns the time elapsed since the given time.
+func (m *Mock) Since(t time.Time) time.Duration {
+	return m.Now().Sub(t)
+}
+
+// Add progresses time by the given duration.
+//
+// It panics if the duration is negative.
+func (m *Mock) Add(d time.Duration) {
+	if d < 0 {
+		panic("cannot add negative duration")
+	}
+	m.now = m.now.Add(d)
+}
diff --git a/scope.go b/scope.go
index d5478ac..b03b508 100644
--- a/scope.go
+++ b/scope.go
@@ -27,6 +27,8 @@ import (
 	"reflect"
 	"sort"
 	"time"
+
+	"go.uber.org/dig/internal/digclock"
 )
 
 // A ScopeOption modifies the default behavior of Scope; currently,
@@ -90,6 +92,9 @@ type Scope struct {
 
 	// All the child scopes of this Scope.
 	childScopes []*Scope
+
+	// clockSrc stores the source of time. Defaults to system clock.
+	clockSrc digclock.Clock
 }
 
 func newScope() *Scope {
@@ -102,6 +107,7 @@ func newScope() *Scope {
 		decoratedGroups: make(map[key]reflect.Value),
 		invokerFn:       defaultInvoker,
 		rand:            rand.New(rand.NewSource(time.Now().UnixNano())),
+		clockSrc:        digclock.System,
 	}
 	s.gh = newGraphHolder(s)
 	return s
@@ -117,6 +123,7 @@ func (s *Scope) Scope(name string, opts ...ScopeOption) *Scope {
 	child.name = name
 	child.parentScope = s
 	child.invokerFn = s.invokerFn
+	child.clockSrc = s.clockSrc
 	child.deferAcyclicVerification = s.deferAcyclicVerification
 	child.recoverFromPanics = s.recoverFromPanics
 
@@ -267,6 +274,10 @@ func (s *Scope) invoker() invokerFn {
 	return s.invokerFn
 }
 
+func (s *Scope) clock() digclock.Clock {
+	return s.clockSrc
+}
+
 // adds a new graphNode to this Scope and all of its descendent
 // scope.
 func (s *Scope) newGraphNode(wrapped interface{}, orders map[*Scope]int) {


Test patch:
diff --git a/dig_int_test.go b/dig_int_test.go
index 4775607..8914081 100644
--- a/dig_int_test.go
+++ b/dig_int_test.go
@@ -20,8 +20,16 @@
 
 package dig
 
-import "math/rand"
+import (
+	"math/rand"
+
+	"go.uber.org/dig/internal/digclock"
+)
 
 func SetRand(r *rand.Rand) Option {
 	return setRand(r)
 }
+
+func SetClock(c digclock.Clock) Option {
+	return setClock(c)
+}
diff --git a/dig_test.go b/dig_test.go
index 5cbf4ee..b8f4a37 100644
--- a/dig_test.go
+++ b/dig_test.go
@@ -34,6 +34,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.uber.org/dig"
+	"go.uber.org/dig/internal/digclock"
 	"go.uber.org/dig/internal/digtest"
 )
 
@@ -1796,6 +1797,55 @@ func TestCallback(t *testing.T) {
 	})
 }
 
+func TestCallbackRuntime(t *testing.T) {
+	t.Run("constructor runtime", func(t *testing.T) {
+		var called bool
+
+		mockClock := digclock.NewMock()
+		c := digtest.New(t, dig.SetClock(mockClock))
+		c.RequireProvide(
+			func() int {
+				mockClock.Add(1 * time.Millisecond)
+				return 5
+			},
+			dig.WithProviderCallback(func(ci dig.CallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.TestCallbackRuntime.func1.1", ci.Name)
+				assert.NoError(t, ci.Error)
+				assert.Equal(t, ci.Runtime, 1*time.Millisecond)
+
+				called = true
+			}),
+		)
+
+		c.Invoke(func(int) {})
+		assert.True(t, called)
+	})
+
+	t.Run("decorator runtime", func(t *testing.T) {
+		var called bool
+
+		mockClock := digclock.NewMock()
+		c := digtest.New(t, dig.SetClock(mockClock))
+		c.RequireProvide(giveInt)
+		c.RequireDecorate(
+			func(int) int {
+				mockClock.Add(1 * time.Millisecond)
+				return 10
+			},
+			dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.TestCallbackRuntime.func2.1", ci.Name)
+				assert.NoError(t, ci.Error)
+				assert.Equal(t, ci.Runtime, 1*time.Millisecond)
+
+				called = true
+			}),
+		)
+
+		c.Invoke(func(int) {})
+		assert.True(t, called)
+	})
+}
+
 func TestProvideConstructorErrors(t *testing.T) {
 	t.Run("multiple-type constructor returns multiple objects of same type", func(t *testing.T) {
 		c := digtest.New(t)
diff --git a/internal/digclock/clock_test.go b/internal/digclock/clock_test.go
new file mode 100644
index 0000000..fda7994
--- /dev/null
+++ b/internal/digclock/clock_test.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2024 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package digclock
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestSystemClock(t *testing.T) {
+	clock := System
+	testClock(t, clock, func(d time.Duration) { time.Sleep(d) })
+}
+
+func TestMockClock(t *testing.T) {
+	clock := NewMock()
+	testClock(t, clock, clock.Add)
+}
+
+func testClock(t *testing.T, clock Clock, advance func(d time.Duration)) {
+	now := clock.Now()
+	assert.False(t, now.IsZero())
+
+	t.Run("Since", func(t *testing.T) {
+		advance(1 * time.Millisecond)
+		assert.NotZero(t, clock.Since(now), "time must have advanced")
+	})
+}
+
+func TestMock_AddNegative(t *testing.T) {
+	clock := NewMock()
+	assert.Panics(t, func() { clock.Add(-1) })
+}