Skip to content

Commit 89f5733

Browse files
authored
feat: add function runtime to dig.CallbackInfo (#412)
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 uber-go/fx#1213 to report runtime of constructors in Run events.
1 parent 11da7b7 commit 89f5733

File tree

9 files changed

+238
-5
lines changed

9 files changed

+238
-5
lines changed

callback.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
package dig
2222

23+
import "time"
24+
2325
// CallbackInfo contains information about a provided function or decorator
2426
// called by Dig, and is passed to a [Callback] registered with
2527
// [WithProviderCallback] or [WithDecoratorCallback].
@@ -32,6 +34,10 @@ type CallbackInfo struct {
3234
// function, if any. When used in conjunction with [RecoverFromPanics],
3335
// this will be set to a [PanicError] when the function panics.
3436
Error error
37+
38+
// Runtime contains the duration it took for the associated
39+
// function to run.
40+
Runtime time.Duration
3541
}
3642

3743
// Callback is a function that can be registered with a provided function

constructor.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,13 @@ func (n *constructorNode) Call(c containerStore) (err error) {
161161
}
162162

163163
if n.callback != nil {
164+
start := c.clock().Now()
164165
// Wrap in separate func to include PanicErrors
165166
defer func() {
166167
n.callback(CallbackInfo{
167-
Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
168-
Error: err,
168+
Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
169+
Error: err,
170+
Runtime: c.clock().Since(start),
169171
})
170172
}()
171173
}

container.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"math/rand"
2626
"reflect"
2727

28+
"go.uber.org/dig/internal/digclock"
2829
"go.uber.org/dig/internal/dot"
2930
)
3031

@@ -141,6 +142,9 @@ type containerStore interface {
141142

142143
// Returns invokerFn function to use when calling arguments.
143144
invoker() invokerFn
145+
146+
// Returns a clock to use
147+
clock() digclock.Clock
144148
}
145149

146150
// New constructs a Container.
@@ -211,6 +215,21 @@ func (o setRandOption) applyOption(c *Container) {
211215
c.scope.rand = o.r
212216
}
213217

218+
// Changes the source of time for the container.
219+
func setClock(c digclock.Clock) Option {
220+
return setClockOption{c: c}
221+
}
222+
223+
type setClockOption struct{ c digclock.Clock }
224+
225+
func (o setClockOption) String() string {
226+
return fmt.Sprintf("setClock(%v)", o.c)
227+
}
228+
229+
func (o setClockOption) applyOption(c *Container) {
230+
c.scope.clockSrc = o.c
231+
}
232+
214233
// DryRun is an Option which, when set to true, disables invocation of functions supplied to
215234
// Provide and Invoke. Use this to build no-op containers.
216235
func DryRun(dry bool) Option {

decorate.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,13 @@ func (n *decoratorNode) Call(s containerStore) (err error) {
122122
}
123123

124124
if n.callback != nil {
125+
start := s.clock().Now()
125126
// Wrap in separate func to include PanicErrors
126127
defer func() {
127128
n.callback(CallbackInfo{
128-
Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
129-
Error: err,
129+
Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
130+
Error: err,
131+
Runtime: s.clock().Since(start),
130132
})
131133
}()
132134
}

dig_int_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@
2020

2121
package dig
2222

23-
import "math/rand"
23+
import (
24+
"math/rand"
25+
26+
"go.uber.org/dig/internal/digclock"
27+
)
2428

2529
func SetRand(r *rand.Rand) Option {
2630
return setRand(r)
2731
}
32+
33+
func SetClock(c digclock.Clock) Option {
34+
return setClock(c)
35+
}

dig_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/stretchr/testify/assert"
3535
"github.com/stretchr/testify/require"
3636
"go.uber.org/dig"
37+
"go.uber.org/dig/internal/digclock"
3738
"go.uber.org/dig/internal/digtest"
3839
)
3940

@@ -1796,6 +1797,55 @@ func TestCallback(t *testing.T) {
17961797
})
17971798
}
17981799

1800+
func TestCallbackRuntime(t *testing.T) {
1801+
t.Run("constructor runtime", func(t *testing.T) {
1802+
var called bool
1803+
1804+
mockClock := digclock.NewMock()
1805+
c := digtest.New(t, dig.SetClock(mockClock))
1806+
c.RequireProvide(
1807+
func() int {
1808+
mockClock.Add(1 * time.Millisecond)
1809+
return 5
1810+
},
1811+
dig.WithProviderCallback(func(ci dig.CallbackInfo) {
1812+
assert.Equal(t, "go.uber.org/dig_test.TestCallbackRuntime.func1.1", ci.Name)
1813+
assert.NoError(t, ci.Error)
1814+
assert.Equal(t, ci.Runtime, 1*time.Millisecond)
1815+
1816+
called = true
1817+
}),
1818+
)
1819+
1820+
c.Invoke(func(int) {})
1821+
assert.True(t, called)
1822+
})
1823+
1824+
t.Run("decorator runtime", func(t *testing.T) {
1825+
var called bool
1826+
1827+
mockClock := digclock.NewMock()
1828+
c := digtest.New(t, dig.SetClock(mockClock))
1829+
c.RequireProvide(giveInt)
1830+
c.RequireDecorate(
1831+
func(int) int {
1832+
mockClock.Add(1 * time.Millisecond)
1833+
return 10
1834+
},
1835+
dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
1836+
assert.Equal(t, "go.uber.org/dig_test.TestCallbackRuntime.func2.1", ci.Name)
1837+
assert.NoError(t, ci.Error)
1838+
assert.Equal(t, ci.Runtime, 1*time.Millisecond)
1839+
1840+
called = true
1841+
}),
1842+
)
1843+
1844+
c.Invoke(func(int) {})
1845+
assert.True(t, called)
1846+
})
1847+
}
1848+
17991849
func TestProvideConstructorErrors(t *testing.T) {
18001850
t.Run("multiple-type constructor returns multiple objects of same type", func(t *testing.T) {
18011851
c := digtest.New(t)

internal/digclock/clock.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright (c) 2024 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package digclock
22+
23+
import (
24+
"time"
25+
)
26+
27+
// Clock defines how dig accesses time.
28+
type Clock interface {
29+
Now() time.Time
30+
Since(time.Time) time.Duration
31+
}
32+
33+
// System is the default implementation of Clock based on real time.
34+
var System Clock = systemClock{}
35+
36+
type systemClock struct{}
37+
38+
func (systemClock) Now() time.Time {
39+
return time.Now()
40+
}
41+
42+
func (systemClock) Since(t time.Time) time.Duration {
43+
return time.Since(t)
44+
}
45+
46+
// Mock is a fake source of time.
47+
// It implements standard time operations, but allows
48+
// the user to control the passage of time.
49+
//
50+
// Use the [Mock.Add] method to progress time.
51+
//
52+
// Note that this implementation is not safe for concurrent use.
53+
type Mock struct {
54+
now time.Time
55+
}
56+
57+
var _ Clock = (*Mock)(nil)
58+
59+
// NewMock creates a new mock clock with the current time set to the current time.
60+
func NewMock() *Mock {
61+
return &Mock{now: time.Now()}
62+
}
63+
64+
// Now returns the current time.
65+
func (m *Mock) Now() time.Time {
66+
return m.now
67+
}
68+
69+
// Since returns the time elapsed since the given time.
70+
func (m *Mock) Since(t time.Time) time.Duration {
71+
return m.Now().Sub(t)
72+
}
73+
74+
// Add progresses time by the given duration.
75+
//
76+
// It panics if the duration is negative.
77+
func (m *Mock) Add(d time.Duration) {
78+
if d < 0 {
79+
panic("cannot add negative duration")
80+
}
81+
m.now = m.now.Add(d)
82+
}

internal/digclock/clock_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2024 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package digclock
22+
23+
import (
24+
"testing"
25+
"time"
26+
27+
"github.com/stretchr/testify/assert"
28+
)
29+
30+
func TestSystemClock(t *testing.T) {
31+
clock := System
32+
testClock(t, clock, func(d time.Duration) { time.Sleep(d) })
33+
}
34+
35+
func TestMockClock(t *testing.T) {
36+
clock := NewMock()
37+
testClock(t, clock, clock.Add)
38+
}
39+
40+
func testClock(t *testing.T, clock Clock, advance func(d time.Duration)) {
41+
now := clock.Now()
42+
assert.False(t, now.IsZero())
43+
44+
t.Run("Since", func(t *testing.T) {
45+
advance(1 * time.Millisecond)
46+
assert.NotZero(t, clock.Since(now), "time must have advanced")
47+
})
48+
}
49+
50+
func TestMock_AddNegative(t *testing.T) {
51+
clock := NewMock()
52+
assert.Panics(t, func() { clock.Add(-1) })
53+
}

scope.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727
"reflect"
2828
"sort"
2929
"time"
30+
31+
"go.uber.org/dig/internal/digclock"
3032
)
3133

3234
// A ScopeOption modifies the default behavior of Scope; currently,
@@ -90,6 +92,9 @@ type Scope struct {
9092

9193
// All the child scopes of this Scope.
9294
childScopes []*Scope
95+
96+
// clockSrc stores the source of time. Defaults to system clock.
97+
clockSrc digclock.Clock
9398
}
9499

95100
func newScope() *Scope {
@@ -102,6 +107,7 @@ func newScope() *Scope {
102107
decoratedGroups: make(map[key]reflect.Value),
103108
invokerFn: defaultInvoker,
104109
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
110+
clockSrc: digclock.System,
105111
}
106112
s.gh = newGraphHolder(s)
107113
return s
@@ -117,6 +123,7 @@ func (s *Scope) Scope(name string, opts ...ScopeOption) *Scope {
117123
child.name = name
118124
child.parentScope = s
119125
child.invokerFn = s.invokerFn
126+
child.clockSrc = s.clockSrc
120127
child.deferAcyclicVerification = s.deferAcyclicVerification
121128
child.recoverFromPanics = s.recoverFromPanics
122129

@@ -267,6 +274,10 @@ func (s *Scope) invoker() invokerFn {
267274
return s.invokerFn
268275
}
269276

277+
func (s *Scope) clock() digclock.Clock {
278+
return s.clockSrc
279+
}
280+
270281
// adds a new graphNode to this Scope and all of its descendent
271282
// scope.
272283
func (s *Scope) newGraphNode(wrapped interface{}, orders map[*Scope]int) {

0 commit comments

Comments
 (0)