report.go ×24

Frontier kind: Code frontier

unlabeled · c_8281ef2b9e16

5 tests · 287 LOC · 5 files · introduces 0 tests · 287 LOC · 5 files

Introduces — evidence that enters the hierarchy at this concept

Code
60 ranges287 lines · 5 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
60 ranges287 lines · 5 files · Browse complete extent
All tests (intent)
5 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Introduced files, introduced tests, and structurally relevant concept specializationreport.go ×3 · 6 introduced LOCreport.go ×3report.go ×3 · 6 introduced LOCreport.go ×3allows_expected_retained_objects · 0 introduced LOCallows_expected_retained…pattern.go ×1 · 2 introduced LOCpattern.go ×1pattern.go ×1 · 5 introduced LOCpattern.go ×1pattern.go ×1 · 2 introduced LOCpattern.go ×1leakcheck.go ×1 · 4 introduced LOCleakcheck.go ×1leakcheck.go ×1 · 1 introduced LOCleakcheck.go ×1pattern.go ×7 · 23 introduced LOCpattern.go ×7report.go ×3 · 7 introduced LOCreport.go ×3report.go ×2 · 8 introduced LOCreport.go ×2TestOperatorServiceMetadata, TestWorkflowServiceMetadata · 0 introduced LOCTestOperatorServiceMetad…go.temporal.io/server/common/testing/objectleak/leakcheck.go · 132 LOCobjectleak/leakcheck.gogo.temporal.io/server/common/testing/objectleak/path.go · 75 LOCobjectleak/path.gogo.temporal.io/server/common/testing/objectleak/pattern.go · 74 LOCobjectleak/pattern.gogo.temporal.io/server/common/testing/objectleak/report.go · 224 LOCobjectleak/report.gogo.temporal.io/server/common/testing/objectleak/walker.go · 116 LOCobjectleak/walker.goTestOperatorServiceMetadata · introduced test · go.temporal.io/server/common/api/TestOperatorServiceMetadataTestOperatorServiceMetad…TestWorkflowServiceMetadata · introduced test · go.temporal.io/server/common/api/TestWorkflowServiceMetadataTestWorkflowServiceMetad…allows_expected_retained_objects · introduced test · go.temporal.io/server/common/testing/objectleak/TestObjectLeak_Check/allows_expected_retained_objectsallows_expected_retained…fails_stale_expected_pattern · introduced test · go.temporal.io/server/common/testing/objectleak/TestObjectLeak_Check/fails_stale_expected_patternfails_stale_expected_pat…fails_stale_prune · introduced test · go.temporal.io/server/common/testing/objectleak/TestObjectLeak_Check/fails_stale_prunefails_stale_pruneprunes_matched_type · introduced test · go.temporal.io/server/common/testing/objectleak/TestObjectLeak_Check/prunes_matched_typeprunes_matched_typereports_retained_objects · introduced test · go.temporal.io/server/common/testing/objectleak/TestObjectLeak_Check/reports_retained_objectsreports_retained_objectsFocused concept · report.go ×24 · 287 introduced LOCreport.go ×24

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

5 files ranked by introduced lines: 287 introduced LOC across 60 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

go.temporal.io/server/common/testing/objectleak/report.go 140 introduced LOC · 24 ranges

Open complete file

30 }
31
32 > func newReport(objects []trackedObject, trackedRoots int, expected patterns, pruneTypes patterns) report { report.go
33 > report := report{
34 > trackedRoots: trackedRoots,
35 > }
36 >
37 > // Matching mutates pattern.matched for stale-expected pattern detection.
38 > activeExpected := slices.Clone(expected)
39 >
40 > type groupKey struct {
41 > path string
42 > typeName string
43 > expected bool
44 > }
45 > groupByKey := make(map[groupKey]*objectGroup)
46 > retainedAddresses := make(map[uintptr]struct{})
47 > expectedAddresses := make(map[uintptr]struct{})
48 > unexpectedAddresses := make(map[uintptr]struct{})
49 >
50 > // Classify each retained object and fold equivalent normalized paths into
51 > // a single report row.
52 > for _, obj := range objects {
53 > if obj.collected.Load() {
54 continue
55 }
56
57 > expectedBy := activeExpected.matchObject(obj) report.go
58 > report.totalRetainedPaths++
59 > retainedAddresses[obj.addr] = struct{}{}
60 > expected := len(expectedBy) > 0
61 > if expected {
62 report.expectedRetainedPaths++
63 expectedAddresses[obj.addr] = struct{}{}
64 > } else { report.go
65 report.unexpectedRetainedPaths++
66 unexpectedAddresses[obj.addr] = struct{}{}
67 }
68
69 > key := groupKey{ report.go
70 > path: obj.path.normalized(),
71 > typeName: obj.typeName,
72 > expected: expected,
73 > }
74 > group := groupByKey[key]
75 > if group == nil {
76 > group = &objectGroup{
77 > path: key.path,
78 > typeName: key.typeName,
79 > addresses: make(map[uintptr]struct{}),
80 > }
81 > groupByKey[key] = group
82 > }
83 > group.paths++
84 > group.addresses[obj.addr] = struct{}{}
85 }
86 > report.totalRetainedObjects = len(retainedAddresses) report.go
87 > report.expectedRetainedObjects = len(expectedAddresses)
88 > report.unexpectedRetainedObjects = len(unexpectedAddresses)
89 >
90 > // Expected patterns that never matched any tracked object are stale and
91 > // should be removed with the fix that made them unnecessary.
92 > report.unmatchedExpected = activeExpected.unmatched()
93 > report.unmatchedPrunes = pruneTypes.unmatched()
94 >
95 > // Keep report output stable across map iteration order and repeated runs.
96 > for key, group := range groupByKey {
97 > if key.expected {
98 report.expectedObjects = append(report.expectedObjects, *group)
99 > } else { report.go
100 report.unexpectedObjects = append(report.unexpectedObjects, *group)
101 }
102 }
103 > sortGroups := func(groups []objectGroup) { report.go
104 > slices.SortFunc(groups, func(a objectGroup, b objectGroup) int {
105 > if c := cmp.Compare(b.objectCount(), a.objectCount()); c != 0 {
106 > return c
107 > }
108 if c := cmp.Compare(b.paths, a.paths); c != 0 {
109 return c
115 })
116 }
117 > sortGroups(report.unexpectedObjects) report.go
118 > sortGroups(report.expectedObjects)
119 > slices.Sort(report.unmatchedExpected)
120 > slices.Sort(report.unmatchedPrunes)
121 > return report
122 }
123
124 > func (r report) failures() error { report.go
125 > var failures []error
126 > if len(r.unexpectedObjects) > 0 {
127 failures = append(failures, errors.New("unexpected retained objects"))
128 }
129 > if len(r.unmatchedExpected) > 0 { report.go
130 failures = append(failures, errors.New("stale expected patterns"))
131 }
132 > if len(r.unmatchedPrunes) > 0 { report.go
133 failures = append(failures, errors.New("stale prunes"))
134 }
135 > return errors.Join(failures...) report.go
136 }
137
138 > func (r report) totals() [3]int { report.go
139 > return [3]int{
140 > r.totalRetainedObjects,
141 > r.unexpectedRetainedObjects,
142 > len(r.unmatchedExpected) + len(r.unmatchedPrunes),
143 > }
144 > }
145
146 > func (r report) string() string { report.go
147 > var out strings.Builder
148 > r.writeSummary(&out)
149 >
150 > writeGroups := func(title string, groups []objectGroup) {
151 > fmt.Fprintf(&out, "%s:\n", title)
152 > if len(groups) == 0 {
153 > out.WriteString(" none\n")
154 > return
155 > }
156 > for _, group := range groups {
157 > fmt.Fprintf(&out, " %s: %s\n", group.counts(), group.name())
158 > }
159 }
160 > out.WriteByte('\n') report.go
161 > writeGroups("unexpected retained objects", r.unexpectedObjects)
162 > out.WriteByte('\n')
163 > writeGroups("expected retained objects", r.expectedObjects)
164 >
165 > if len(r.unmatchedExpected) > 0 {
166 out.WriteString("\nstale expected patterns:\n")
167 }
168 > for _, pattern := range r.unmatchedExpected { report.go
169 fmt.Fprintf(&out, " %s\n", pattern)
170 }
171
172 > if len(r.unmatchedPrunes) > 0 { report.go
173 out.WriteString("\nstale prunes:\n")
174 }
175 > for _, pattern := range r.unmatchedPrunes { report.go
176 fmt.Fprintf(&out, " %s\n", pattern)
177 }
178 > return strings.TrimSuffix(out.String(), "\n") report.go
179 }
180
181 > func (r report) writeSummary(out *strings.Builder) { report.go
182 > out.WriteString("object leak report\n\n")
183 > fmt.Fprintf(out, "tracked root objects: %d\n", r.trackedRoots)
184 > fmt.Fprintf(
185 > out,
186 > "retained paths: %d total, %d expected, %d unexpected\n",
187 > r.totalRetainedPaths,
188 > r.expectedRetainedPaths,
189 > r.unexpectedRetainedPaths,
190 > )
191 > fmt.Fprintf(
192 > out,
193 > "retained objects: %d total, %d expected, %d unexpected\n",
194 > r.totalRetainedObjects,
195 > r.expectedRetainedObjects,
196 > r.unexpectedRetainedObjects,
197 > )
198 > }
199
200 > func (g objectGroup) name() string { report.go
201 > if g.path == "" {
202 > return g.typeName
203 > }
204 > return fmt.Sprintf("%s (%s)", g.path, g.typeName)
205 }
206
207 > func (g objectGroup) objectCount() int { report.go
208 > return len(g.addresses)
209 > }
210
211 > func (g objectGroup) counts() string { report.go
212 > objects := g.objectCount()
213 > if g.paths == objects {
214 > return formatCount(objects, "object")
215 > }
216 > return fmt.Sprintf("%s, %s", formatCount(g.paths, "path"), formatCount(objects, "object"))
217 }
218
219 > func formatCount(count int, label string) string { report.go
220 > if count == 1 {
221 > return fmt.Sprintf("1 %s", label)
222 > }
223 > return fmt.Sprintf("%d %ss", count, label)
224 }
go.temporal.io/server/common/testing/objectleak/leakcheck.go 61 introduced LOC · 9 ranges

Open complete file

30 // WithExpected marks retained objects whose reflected path or type name matches
31 // pattern as expected. A trailing '*' matches any suffix.
32 > func WithExpected(pattern string) Option { leakcheck.go
33 > return func(t *ObjectLeakCheck) error {
34 t.expected = append(t.expected, newPattern(pattern))
35 return nil
40 // matches pattern. Named types match their package-qualified name. A trailing
41 // '*' matches any suffix.
42 > func WithPruneType(pattern string) Option { leakcheck.go
43 > return func(t *ObjectLeakCheck) error {
44 t.pruneTypes = append(t.pruneTypes, newPattern(pattern))
45 return nil
49 // WithGCSettleTimeout sets the maximum time Check spends forcing GC and waiting
50 // for retained-object counts to settle.
51 > func WithGCSettleTimeout(timeout time.Duration) Option { leakcheck.go
52 > return func(t *ObjectLeakCheck) error {
53 > if timeout <= 0 {
54 return errors.New("GC settle timeout must be positive")
55 }
56 > t.gcSettleTimeout = timeout leakcheck.go
57 > return nil
58 }
59 }
60
61 // NewObjectLeakCheck creates an object leak checker.
62 > func NewObjectLeakCheck(opts ...Option) (ObjectLeakCheck, error) { leakcheck.go
63 > t := ObjectLeakCheck{
64 > gcSettleTimeout: defaultGCSettleTimeout,
65 > }
66 > for _, opt := range opts {
67 > if err := opt(&t); err != nil {
68 return ObjectLeakCheck{}, err
69 }
70 }
71 > return t, nil leakcheck.go
72 }
73
74 // Track walks all values reachable from root and tracks pointer objects it finds.
75 > func (t *ObjectLeakCheck) Track(root any) { leakcheck.go
76 > walker := newObjectWalker(t.pruneTypes)
77 > walker.track(root)
78 > t.roots++
79 > t.objects = append(t.objects, walker.objects...)
80 > }
81
82 // Check settles GC, then returns a full retained-object report and an error for
84 // longer match any tracked object, or prune rules that did not match during
85 // tracking.
86 > func (t *ObjectLeakCheck) Check() (string, error) { leakcheck.go
87 > start := time.Now()
88 > minWaitDeadline := start.Add(checkGCMinWait)
89 > deadline := start.Add(t.gcSettleTimeout)
90 > settledDeadline := minWaitDeadline
91 >
92 > var lastTotals [3]int
93 > var haveLastTotals bool
94 > var report report
95 > var err error
96 > for {
97 > // AddCleanup callbacks run after GC proves tracked objects are
98 > // unreachable. Run a small burst and yield so callbacks can mark tracked
99 > // objects before the next report snapshot.
100 > for range checkGCBurst {
101 > //nolint:revive // This checker intentionally forces GC to drive AddCleanup callbacks.
102 > runtime.GC()
103 > runtime.Gosched()
104 > }
105 > runtimedebug.FreeOSMemory()
106 > runtime.Gosched()
107 >
108 > report = newReport(t.objects, t.roots, t.expected, t.pruneTypes)
109 > err = report.failures()
110 > now := time.Now()
111 >
112 > // Wait for the report totals to stop changing instead of returning on
113 > // the first passing report. This lets delayed cleanup callbacks remove
114 > // both unexpected objects and now-stale expected patterns before we decide.
115 > if totals := report.totals(); !haveLastTotals || totals != lastTotals {
116 > lastTotals = totals
117 > haveLastTotals = true
118 > settledDeadline = now.Add(checkGCQuiet)
119 > if minWaitDeadline.After(settledDeadline) {
120 > settledDeadline = minWaitDeadline
121 > }
122 }
123
125 // quiet window then handles normal cleanup latency; the timeout bounds a
126 // genuinely stuck object graph so the test can still report diagnostics.
127 > if now.After(settledDeadline) || now.After(deadline) { leakcheck.go
128 > return report.string(), err
129 > }
130 time.Sleep(checkGCPause)
131 }
go.temporal.io/server/common/testing/objectleak/walker.go 50 introduced LOC · 14 ranges

Open complete file

22 }
23
24 > func newObjectWalker(pruneTypes patterns) objectWalker { walker.go
25 > return objectWalker{
26 > seen: make(map[uintptr]struct{}),
27 > pruneTypes: pruneTypes,
28 > }
29 > }
30
31 > func (w *objectWalker) track(root any) { walker.go
32 > w.walk(reflect.ValueOf(root), nil)
33 > }
34
35 > func (w *objectWalker) walk(v reflect.Value, path path) { walker.go
36 > if !v.IsValid() {
37 return
38 }
39 > for v.Kind() == reflect.Interface { walker.go
40 if v.IsNil() {
41 return
43 v = v.Elem()
44 }
45 > switch v.Kind() { walker.go
46 > case reflect.Pointer:
47 > if v.IsNil() {
48 return
49 }
50 > ptr := v.UnsafePointer() walker.go
51 > addr := uintptr(ptr)
52 > if _, ok := w.seen[addr]; ok {
53 return
54 }
55 > w.seen[addr] = struct{}{} walker.go
56 > if obj, ok := trackPointerObject(ptr, addr, path, v.Type().String()); ok {
57 > w.objects = append(w.objects, obj)
58 > }
59 > if w.shouldPrune(v.Type()) {
60 return
61 }
62 > w.walk(v.Elem(), path) walker.go
63 > case reflect.Struct:
64 > if w.shouldPrune(v.Type()) {
65 return
66 }
67 > for i := 0; i < v.NumField(); i++ { walker.go
68 > field := v.Type().Field(i)
69 > w.walk(v.Field(i), path.field(field.Name))
70 > }
71 case reflect.Slice, reflect.Array:
72 for i := 0; i < v.Len(); i++ {
84 }
85
86 > func (w *objectWalker) shouldPrune(t reflect.Type) bool { walker.go
87 > return w.pruneTypes.matchType(t)
88 > }
89
90 > func trackPointerObject(ptr unsafe.Pointer, addr uintptr, path path, typeName string) (trackedObject, bool) { walker.go
91 > collected := &atomic.Bool{}
92 > var cleanup runtime.Cleanup
93 > ok := true
94 > func() {
95 > // Some reflected pointers are not valid heap objects for AddCleanup.
96 > defer func() {
97 > if recover() != nil {
98 ok = false
99 }
100 }()
101 //nolint:govet // The checker must attach cleanup to reflected heap addresses.
102 > cleanup = runtime.AddCleanup((*byte)(ptr), func(collected *atomic.Bool) { walker.go
103 collected.Store(true)
104 }, collected)
105 }()
106 > if !ok || cleanup == (runtime.Cleanup{}) { walker.go
107 return trackedObject{}, false
108 }
109 > return trackedObject{ walker.go
110 > addr: addr,
111 > path: path,
112 > typeName: typeName,
113 > collected: collected,
114 > cleanup: cleanup,
115 > }, true
116 }
go.temporal.io/server/common/testing/objectleak/path.go 18 introduced LOC · 6 ranges

Open complete file

22 }
23
24 > func (p path) field(name string) path { path.go
25 > return p.append(pathSegment{kind: pathSegmentField, value: name})
26 > }
27
28 func (p path) index(index int) path {
34 }
35
36 > func (p path) append(segment pathSegment) path { path.go
37 > // Each recursion branch owns its path so sibling fields cannot mutate it.
38 > return append(slices.Clone(p), segment)
39 > }
40
41 > func (p path) normalized() string { path.go
42 > return p.format(true)
43 > }
44
45 > func (p path) format(normalized bool) string { path.go
46 > var out strings.Builder
47 > for i, segment := range p {
48 > switch segment.kind {
49 > case pathSegmentField:
50 > if i > 0 {
51 out.WriteByte('.')
52 }
53 > out.WriteString(segment.value) path.go
54 case pathSegmentIndex:
55 if normalized {
72 }
73 }
74 > return out.String() path.go
75 }
go.temporal.io/server/common/testing/objectleak/pattern.go 18 introduced LOC · 7 ranges

Open complete file

33 }
34
35 > func (ps patterns) matchObject(obj trackedObject) []string { pattern.go
36 > var matches []string
37 > for i := range ps {
38 if ps[i].matchesObject(obj) {
39 ps[i].matched = true
41 }
42 }
43 > return matches pattern.go
44 }
45
46 > func (ps patterns) matchAny(values ...string) bool { pattern.go
47 > for i := range ps {
48 if slices.ContainsFunc(values, ps[i].matches) {
49 ps[i].matched = true
51 }
52 }
53 > return false pattern.go
54 }
55
56 > func (ps patterns) matchType(t reflect.Type) bool { pattern.go
57 > for t.Kind() == reflect.Pointer {
58 > t = t.Elem()
59 > }
60 > if t.Name() != "" {
61 > return ps.matchAny(t.PkgPath() + "." + t.Name())
62 > }
63 return ps.matchAny(t.String())
64 }
65
66 > func (ps patterns) unmatched() []string { pattern.go
67 > var unmatched []string
68 > for _, pattern := range ps {
69 if !pattern.matched {
70 unmatched = append(unmatched, pattern.String())
71 }
72 }
73 > return unmatched pattern.go
74 }