1
2
3
4
5 package counter
6
7 import (
8 "bytes"
9 "errors"
10 "fmt"
11 "math/rand"
12 "os"
13 "path"
14 "path/filepath"
15 "runtime"
16 "runtime/debug"
17 "sync"
18 "sync/atomic"
19 "time"
20 "unsafe"
21
22 "golang.org/x/telemetry/internal/mmap"
23 "golang.org/x/telemetry/internal/telemetry"
24 )
25
26
27 type file struct {
28
29
30
31
32 counters atomic.Pointer[Counter]
33 end Counter
34
35 mu sync.Mutex
36 buildInfo *debug.BuildInfo
37 timeBegin, timeEnd time.Time
38 err error
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54 current atomic.Pointer[mappedFile]
55 }
56
57 var defaultFile file
58
59
60 func (f *file) register(c *Counter) {
61 debugPrintf("register %s %p\n", c.Name(), c)
62
63
64
65
66
67 wroteNext := false
68 for wroteNext || c.next.Load() == nil {
69 head := f.counters.Load()
70 next := head
71 if next == nil {
72 next = &f.end
73 }
74 debugPrintf("register %s next %p\n", c.Name(), next)
75 if !wroteNext {
76 if !c.next.CompareAndSwap(nil, next) {
77 debugPrintf("register %s cas failed %p\n", c.Name(), c.next.Load())
78 continue
79 }
80 wroteNext = true
81 } else {
82 c.next.Store(next)
83 }
84 if f.counters.CompareAndSwap(head, c) {
85 debugPrintf("registered %s %p\n", c.Name(), f.counters.Load())
86 return
87 }
88 debugPrintf("register %s cas2 failed %p %p\n", c.Name(), f.counters.Load(), head)
89 }
90 }
91
92
93
94
95
96
97 func (f *file) invalidateCounters() {
98
99 if head := f.counters.Load(); head != nil {
100 for c := head; c != &f.end; c = c.next.Load() {
101 c.invalidate()
102 }
103 for c := head; c != &f.end; c = c.next.Load() {
104 c.refresh()
105 }
106 }
107 }
108
109
110
111
112
113 func (f *file) lookup(name string) counterPtr {
114 current := f.current.Load()
115 if current == nil {
116 debugPrintf("lookup %s - no mapped file\n", name)
117 return counterPtr{}
118 }
119 ptr := f.newCounter(name)
120 if ptr == nil {
121 return counterPtr{}
122 }
123 return counterPtr{current, ptr}
124 }
125
126
127 var ErrDisabled = errors.New("counter: disabled as Go telemetry is off")
128
129 var (
130 errNoBuildInfo = errors.New("counter: missing build info")
131 errCorrupt = errors.New("counter: corrupt counter file")
132 )
133
134
135
136
137
138 func weekEnd() (time.Weekday, error) {
139
140
141
142 weekends := filepath.Join(telemetry.Default.LocalDir(), "weekends")
143 day := fmt.Sprintf("%d\n", rand.Intn(7))
144 if _, err := os.ReadFile(weekends); err != nil {
145 if err := os.MkdirAll(telemetry.Default.LocalDir(), 0777); err != nil {
146 debugPrintf("%v: could not create telemetry.LocalDir %s", err, telemetry.Default.LocalDir())
147 return 0, err
148 }
149 if err = os.WriteFile(weekends, []byte(day), 0666); err != nil {
150 return 0, err
151 }
152 }
153
154
155 buf, err := os.ReadFile(weekends)
156
157
158 if err != nil {
159 return 0, err
160 }
161 buf = bytes.TrimSpace(buf)
162 if len(buf) == 0 {
163 return 0, fmt.Errorf("empty weekends file")
164 }
165 weekend := time.Weekday(buf[0] - '0')
166
167 weekend %= 7
168 if weekend < 0 {
169 weekend += 7
170 }
171 return weekend, nil
172 }
173
174
175
176
177
178
179 func (f *file) rotate() {
180 expiry := f.rotate1()
181 if !expiry.IsZero() {
182 delay := time.Until(expiry)
183
184
185
186
187 const minDelay = 1 * time.Minute
188 if delay < minDelay {
189 delay = minDelay
190 }
191
192 time.AfterFunc(delay, f.rotate)
193 }
194 }
195
196 func nop() {}
197
198
199
200 var CounterTime = func() time.Time {
201 return time.Now().UTC()
202 }
203
204
205
206 func counterSpan() (begin, end time.Time, _ error) {
207 year, month, day := CounterTime().Date()
208 begin = time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
209
210
211 weekend, err := weekEnd()
212 if err != nil {
213 return time.Time{}, time.Time{}, err
214 }
215 incr := int(weekend - begin.Weekday())
216 if incr <= 0 {
217 incr += 7
218 }
219 end = time.Date(year, month, day+incr, 0, 0, 0, 0, time.UTC)
220 return begin, end, nil
221 }
222
223
224
225 func (f *file) rotate1() time.Time {
226
227
228 var previous *mappedFile
229 defer func() {
230
231 if next := f.current.Load(); next != previous {
232 f.invalidateCounters()
233
234 if previous != nil {
235 previous.close()
236 }
237 }
238 }()
239
240 f.mu.Lock()
241 defer f.mu.Unlock()
242
243 previous = f.current.Load()
244
245 if f.err != nil {
246 return time.Time{}
247 }
248
249 fail := func(err error) {
250 debugPrintf("rotate: %v", err)
251 f.err = err
252 f.current.Store(nil)
253 }
254
255 if mode, _ := telemetry.Default.Mode(); mode == "off" {
256
257
258
259 fail(ErrDisabled)
260 return time.Time{}
261 }
262
263 if f.buildInfo == nil {
264 bi, ok := debug.ReadBuildInfo()
265 if !ok {
266 fail(errNoBuildInfo)
267 return time.Time{}
268 }
269 f.buildInfo = bi
270 }
271
272 begin, end, err := counterSpan()
273 if err != nil {
274 fail(err)
275 return time.Time{}
276 }
277 if f.timeBegin.Equal(begin) && f.timeEnd.Equal(end) {
278 return f.timeEnd
279 }
280 f.timeBegin, f.timeEnd = begin, end
281
282 goVers, progPath, progVers := telemetry.ProgramInfo(f.buildInfo)
283 meta := fmt.Sprintf("TimeBegin: %s\nTimeEnd: %s\nProgram: %s\nVersion: %s\nGoVersion: %s\nGOOS: %s\nGOARCH: %s\n\n",
284 f.timeBegin.Format(time.RFC3339), f.timeEnd.Format(time.RFC3339),
285 progPath, progVers, goVers, runtime.GOOS, runtime.GOARCH)
286 if len(meta) > maxMetaLen {
287 fail(fmt.Errorf("metadata too long"))
288 return time.Time{}
289 }
290
291 if progVers != "" {
292 progVers = "@" + progVers
293 }
294 baseName := fmt.Sprintf("%s%s-%s-%s-%s-%s.%s.count",
295 path.Base(progPath),
296 progVers,
297 goVers,
298 runtime.GOOS,
299 runtime.GOARCH,
300 f.timeBegin.Format(time.DateOnly),
301 FileVersion,
302 )
303 dir := telemetry.Default.LocalDir()
304 if err := os.MkdirAll(dir, 0777); err != nil {
305 fail(fmt.Errorf("making local dir: %v", err))
306 return time.Time{}
307 }
308 name := filepath.Join(dir, baseName)
309
310 m, err := openMapped(name, meta)
311 if err != nil {
312
313
314
315
316 fail(fmt.Errorf("openMapped: %v", err))
317 return time.Time{}
318 }
319
320 debugPrintf("using %v", m.f.Name())
321 f.current.Store(m)
322 return f.timeEnd
323 }
324
325 func (f *file) newCounter(name string) *atomic.Uint64 {
326 v, cleanup := f.newCounter1(name)
327 cleanup()
328 return v
329 }
330
331 func (f *file) newCounter1(name string) (v *atomic.Uint64, cleanup func()) {
332 f.mu.Lock()
333 defer f.mu.Unlock()
334
335 current := f.current.Load()
336 if current == nil {
337 return nil, nop
338 }
339 debugPrintf("newCounter %s in %s\n", name, current.f.Name())
340 if v, _, _, _ := current.lookup(name); v != nil {
341 return v, nop
342 }
343 v, newM, err := current.newCounter(name)
344 if err != nil {
345 debugPrintf("newCounter %s: %v\n", name, err)
346 return nil, nop
347 }
348
349 cleanup = nop
350 if newM != nil {
351 f.current.Store(newM)
352 cleanup = func() {
353 f.invalidateCounters()
354 current.close()
355 }
356 }
357 return v, cleanup
358 }
359
360 var (
361 openOnce sync.Once
362
363
364
365
366
367
368
369 rotating bool
370 )
371
372
373
374
375
376
377 func Open(rotate bool) func() {
378 if telemetry.DisabledOnPlatform {
379 return func() {}
380 }
381 close := func() {}
382 openOnce.Do(func() {
383 rotating = rotate
384 if mode, _ := telemetry.Default.Mode(); mode == "off" {
385
386 defaultFile.err = ErrDisabled
387
388 return
389 }
390 debugPrintf("Open(%v)", rotate)
391 if rotate {
392 defaultFile.rotate()
393 } else {
394 defaultFile.rotate1()
395 }
396 close = func() {
397
398 mf := defaultFile.current.Load()
399 if mf == nil {
400
401 return
402 }
403 mf.close()
404 }
405 })
406 if rotating != rotate {
407 panic("BUG: Open called with inconsistent values for 'rotate'")
408 }
409 return close
410 }
411
412 const (
413 FileVersion = "v1"
414 hdrPrefix = "# telemetry/counter file " + FileVersion + "\n"
415 recordUnit = 32
416 maxMetaLen = 512
417 numHash = 512
418 maxNameLen = 4 * 1024
419 limitOff = 0
420 hashOff = 4
421 pageSize = 16 * 1024
422 minFileLen = 16 * 1024
423 )
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444 type mappedFile struct {
445 meta string
446 hdrLen uint32
447 zero [4]byte
448 closeOnce sync.Once
449 f *os.File
450 mapping *mmap.Data
451 }
452
453
454
455
456
457
458
459
460
461
462 func openMapped(name, meta string) (_ *mappedFile, err error) {
463 hdr, err := mappedHeader(meta)
464 if err != nil {
465 return nil, err
466 }
467
468 f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
469 if err != nil {
470 return nil, err
471 }
472
473
474 m := &mappedFile{
475 f: f,
476 meta: meta,
477 }
478
479 defer func() {
480 if err != nil {
481 m.close()
482 }
483 }()
484
485 info, err := f.Stat()
486 if err != nil {
487 return nil, err
488 }
489
490
491 if info.Size() < minFileLen {
492 if _, err := f.WriteAt(hdr, 0); err != nil {
493 return nil, err
494 }
495
496 if _, err := f.WriteAt(m.zero[:], int64(minFileLen-len(m.zero))); err != nil {
497 return nil, err
498 }
499 info, err = f.Stat()
500 if err != nil {
501 return nil, err
502 }
503 if info.Size() < minFileLen {
504 return nil, fmt.Errorf("counter: writing file did not extend it")
505 }
506 }
507
508
509 mapping, err := memmap(f)
510 if err != nil {
511 return nil, err
512 }
513 m.mapping = mapping
514 if !bytes.HasPrefix(m.mapping.Data, hdr) {
515
516
517 return nil, fmt.Errorf("counter: header mismatch")
518 }
519 m.hdrLen = uint32(len(hdr))
520
521 return m, nil
522 }
523
524 func mappedHeader(meta string) ([]byte, error) {
525 if len(meta) > maxMetaLen {
526 return nil, fmt.Errorf("counter: metadata too large")
527 }
528 np := round(len(hdrPrefix), 4)
529 n := round(np+4+len(meta), 32)
530 hdr := make([]byte, n)
531 copy(hdr, hdrPrefix)
532 *(*uint32)(unsafe.Pointer(&hdr[np])) = uint32(n)
533 copy(hdr[np+4:], meta)
534 return hdr, nil
535 }
536
537 func (m *mappedFile) place(limit uint32, name string) (start, end uint32) {
538 if limit == 0 {
539
540 limit = m.hdrLen + hashOff + 4*numHash
541 }
542 n := round(uint32(16+len(name)), recordUnit)
543 start = round(limit, recordUnit)
544
545
546
547
548
549 if start/pageSize != (start+n)/pageSize {
550
551 start = round(limit, pageSize)
552 }
553 return start, start + n
554 }
555
556 var memmap = mmap.Mmap
557 var munmap = mmap.Munmap
558
559 func (m *mappedFile) close() {
560 m.closeOnce.Do(func() {
561 if m.mapping != nil {
562 munmap(m.mapping)
563 m.mapping = nil
564 }
565 if m.f != nil {
566 m.f.Close()
567 m.f = nil
568 }
569 })
570 }
571
572
573
574
575
576 func hash(name string) uint32 {
577 const (
578 offset32 = 2166136261
579 prime32 = 16777619
580 )
581 h := uint32(offset32)
582 for i := 0; i < len(name); i++ {
583 c := name[i]
584 h = (h ^ uint32(c)) * prime32
585 }
586 return (h ^ (h >> 16)) % numHash
587 }
588
589 func (m *mappedFile) load32(off uint32) uint32 {
590 if int64(off) >= int64(len(m.mapping.Data)) {
591 return 0
592 }
593 return (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off])).Load()
594 }
595
596 func (m *mappedFile) cas32(off, old, new uint32) bool {
597 if int64(off) >= int64(len(m.mapping.Data)) {
598 panic("bad cas32")
599 }
600 return (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off])).CompareAndSwap(old, new)
601 }
602
603
604
605
606 func (m *mappedFile) entryAt(off uint32) (name []byte, next uint32, v *atomic.Uint64, ok bool) {
607 if off < m.hdrLen+hashOff || int64(off)+16 > int64(len(m.mapping.Data)) {
608 return nil, 0, nil, false
609 }
610 nameLen := m.load32(off+8) & 0x00ffffff
611 if nameLen == 0 || int64(off)+16+int64(nameLen) > int64(len(m.mapping.Data)) {
612 return nil, 0, nil, false
613 }
614 name = m.mapping.Data[off+16 : off+16+nameLen]
615 next = m.load32(off + 12)
616 v = (*atomic.Uint64)(unsafe.Pointer(&m.mapping.Data[off]))
617 return name, next, v, true
618 }
619
620
621
622
623
624
625
626 func (m *mappedFile) writeEntryAt(off uint32, name string) (next *atomic.Uint32, v *atomic.Uint64, ok bool) {
627
628 if off < m.hdrLen+hashOff || int64(off)+16+int64(len(name)) > int64(len(m.mapping.Data)) {
629 return nil, nil, false
630 }
631 copy(m.mapping.Data[off+16:], name)
632 atomic.StoreUint32((*uint32)(unsafe.Pointer(&m.mapping.Data[off+8])), uint32(len(name))|0xff000000)
633 next = (*atomic.Uint32)(unsafe.Pointer(&m.mapping.Data[off+12]))
634 v = (*atomic.Uint64)(unsafe.Pointer(&m.mapping.Data[off]))
635 return next, v, true
636 }
637
638
639
640
641
642
643 func (m *mappedFile) lookup(name string) (v *atomic.Uint64, headOff, head uint32, ok bool) {
644 h := hash(name)
645 headOff = m.hdrLen + hashOff + h*4
646 head = m.load32(headOff)
647 off := head
648 for off != 0 {
649 ename, next, v, ok := m.entryAt(off)
650 if !ok {
651 return nil, 0, 0, false
652 }
653 if string(ename) == name {
654 return v, headOff, head, true
655 }
656 off = next
657 }
658 return nil, headOff, head, true
659 }
660
661
662
663
664 func (m *mappedFile) newCounter(name string) (v *atomic.Uint64, m1 *mappedFile, err error) {
665 if len(name) > maxNameLen {
666 return nil, nil, fmt.Errorf("counter name too long")
667 }
668 orig := m
669 defer func() {
670 if m != orig {
671 if err != nil {
672 m.close()
673 } else {
674 m1 = m
675 }
676 }
677 }()
678
679 v, headOff, head, ok := m.lookup(name)
680 for tries := 0; !ok; tries++ {
681 if tries >= 10 {
682 debugFatalf("corrupt: failed to remap after 10 tries")
683 return nil, nil, errCorrupt
684 }
685
686
687 limit := m.load32(m.hdrLen + limitOff)
688 if limit, datalen := int64(limit), int64(len(m.mapping.Data)); limit <= datalen {
689
690
691
692
693 debugFatalf("corrupt: limit %d is within mapping length %d", limit, datalen)
694 return nil, nil, errCorrupt
695 }
696
697
698 newM, err := openMapped(m.f.Name(), m.meta)
699 if err != nil {
700 return nil, nil, err
701 }
702 if limit, datalen := int64(limit), int64(len(newM.mapping.Data)); limit > datalen {
703
704
705
706 debugFatalf("corrupt: limit %d exceeds file size %d", limit, datalen)
707 return nil, nil, errCorrupt
708 }
709
710
711 if m != orig {
712 m.close()
713 }
714 m = newM
715 v, headOff, head, ok = m.lookup(name)
716 }
717 if v != nil {
718 return v, nil, nil
719 }
720
721
722
723
724 var start, end uint32
725 for {
726
727 limit := m.load32(m.hdrLen + limitOff)
728 start, end = m.place(limit, name)
729 debugPrintf("place %s at %#x-%#x\n", name, start, end)
730 if int64(end) > int64(len(m.mapping.Data)) {
731 newM, err := m.extend(end)
732 if err != nil {
733 return nil, nil, err
734 }
735 if m != orig {
736 m.close()
737 }
738 m = newM
739 continue
740 }
741
742
743 if m.cas32(m.hdrLen+limitOff, limit, end) {
744 break
745 }
746 }
747
748
749 next, v, ok := m.writeEntryAt(start, name)
750 if !ok {
751 debugFatalf("corrupt: failed to write entry: %#x+%d vs %#x\n", start, len(name), len(m.mapping.Data))
752 return nil, nil, errCorrupt
753 }
754
755
756
757 for {
758 next.Store(head)
759 if m.cas32(headOff, head, start) {
760 return v, nil, nil
761 }
762
763
764 old := head
765 head = m.load32(headOff)
766 for off := head; off != old; {
767 ename, enext, v, ok := m.entryAt(off)
768 if !ok {
769 return nil, nil, errCorrupt
770 }
771 if string(ename) == name {
772 next.Store(^uint32(0))
773 return v, nil, nil
774 }
775 off = enext
776 }
777 }
778 }
779
780 func (m *mappedFile) extend(end uint32) (*mappedFile, error) {
781 end = round(end, pageSize)
782 info, err := m.f.Stat()
783 if err != nil {
784 return nil, err
785 }
786 if info.Size() < int64(end) {
787
788
789
790
791
792
793 if _, err := m.f.WriteAt(m.zero[:], int64(end)-int64(len(m.zero))); err != nil {
794 return nil, err
795 }
796 }
797 newM, err := openMapped(m.f.Name(), m.meta)
798 if err != nil {
799 return nil, err
800 }
801 if int64(len(newM.mapping.Data)) < int64(end) {
802
803
804 newM.close()
805 return nil, errCorrupt
806 }
807 return newM, err
808 }
809
810
811
812 func round[T int | uint32](x T, unit T) T {
813 return (x + unit - 1) &^ (unit - 1)
814 }
815
View as plain text