20259: Add documentation for banner and tooltip features
[arvados.git] / lib / crunchstat / crunchstat.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 // Package crunchstat reports resource usage (CPU, memory, disk,
6 // network) for a cgroup.
7 package crunchstat
8
9 import (
10         "bufio"
11         "bytes"
12         "errors"
13         "fmt"
14         "io"
15         "io/ioutil"
16         "os"
17         "regexp"
18         "sort"
19         "strconv"
20         "strings"
21         "sync"
22         "syscall"
23         "time"
24 )
25
26 // crunchstat collects all memory statistics, but only reports these.
27 var memoryStats = [...]string{"cache", "swap", "pgmajfault", "rss"}
28
29 type logPrinter interface {
30         Printf(fmt string, args ...interface{})
31 }
32
33 // A Reporter gathers statistics for a cgroup and writes them to a
34 // log.Logger.
35 type Reporter struct {
36         // CID of the container to monitor. If empty, read the CID
37         // from CIDFile (first waiting until a non-empty file appears
38         // at CIDFile). If CIDFile is also empty, report host
39         // statistics.
40         CID string
41
42         // Path to a file we can read CID from.
43         CIDFile string
44
45         // Where cgroup accounting files live on this system, e.g.,
46         // "/sys/fs/cgroup".
47         CgroupRoot string
48
49         // Parent cgroup, e.g., "docker".
50         CgroupParent string
51
52         // Interval between samples. Must be positive.
53         PollPeriod time.Duration
54
55         // Temporary directory, will be monitored for available, used & total space.
56         TempDir string
57
58         // Where to write statistics. Must not be nil.
59         Logger logPrinter
60
61         // When stats cross thresholds configured in the fields below,
62         // they are reported to this logger.
63         ThresholdLogger logPrinter
64
65         // MemThresholds maps memory stat names to slices of thresholds.
66         // When the corresponding stat exceeds a threshold, that will be logged.
67         MemThresholds map[string][]Threshold
68
69         kernelPageSize      int64
70         reportedStatFile    map[string]string
71         lastNetSample       map[string]ioSample
72         lastDiskIOSample    map[string]ioSample
73         lastCPUSample       cpuSample
74         lastDiskSpaceSample diskSpaceSample
75         lastMemSample       memSample
76         maxDiskSpaceSample  diskSpaceSample
77         maxMemSample        map[memoryKey]int64
78
79         reportPIDs   map[string]int
80         reportPIDsMu sync.Mutex
81
82         done    chan struct{} // closed when we should stop reporting
83         flushed chan struct{} // closed when we have made our last report
84 }
85
86 type Threshold struct {
87         percentage int64
88         threshold  int64
89         total      int64
90 }
91
92 func NewThresholdFromPercentage(total int64, percentage int64) Threshold {
93         return Threshold{
94                 percentage: percentage,
95                 threshold:  total * percentage / 100,
96                 total:      total,
97         }
98 }
99
100 func NewThresholdsFromPercentages(total int64, percentages []int64) (thresholds []Threshold) {
101         for _, percentage := range percentages {
102                 thresholds = append(thresholds, NewThresholdFromPercentage(total, percentage))
103         }
104         return
105 }
106
107 // memoryKey is a key into Reporter.maxMemSample.
108 // Initialize it with just statName to get the host/cgroup maximum.
109 // Initialize it with all fields to get that process' maximum.
110 type memoryKey struct {
111         processID   int
112         processName string
113         statName    string
114 }
115
116 // Start starts monitoring in a new goroutine, and returns
117 // immediately.
118 //
119 // The monitoring goroutine waits for a non-empty CIDFile to appear
120 // (unless CID is non-empty). Then it waits for the accounting files
121 // to appear for the monitored container. Then it collects and reports
122 // statistics until Stop is called.
123 //
124 // Callers should not call Start more than once.
125 //
126 // Callers should not modify public data fields after calling Start.
127 func (r *Reporter) Start() {
128         r.done = make(chan struct{})
129         r.flushed = make(chan struct{})
130         go r.run()
131 }
132
133 // ReportPID starts reporting stats for a specified process.
134 func (r *Reporter) ReportPID(name string, pid int) {
135         r.reportPIDsMu.Lock()
136         defer r.reportPIDsMu.Unlock()
137         if r.reportPIDs == nil {
138                 r.reportPIDs = map[string]int{name: pid}
139         } else {
140                 r.reportPIDs[name] = pid
141         }
142 }
143
144 // Stop reporting. Do not call more than once, or before calling
145 // Start.
146 //
147 // Nothing will be logged after Stop returns unless you call a Log* method.
148 func (r *Reporter) Stop() {
149         close(r.done)
150         <-r.flushed
151 }
152
153 func (r *Reporter) reportMemoryMax(logger logPrinter, source, statName string, value, limit int64) {
154         var units string
155         switch statName {
156         case "pgmajfault":
157                 units = "faults"
158         default:
159                 units = "bytes"
160         }
161         if limit > 0 {
162                 percentage := 100 * value / limit
163                 logger.Printf("Maximum %s memory %s usage was %d%%, %d/%d %s",
164                         source, statName, percentage, value, limit, units)
165         } else {
166                 logger.Printf("Maximum %s memory %s usage was %d %s",
167                         source, statName, value, units)
168         }
169 }
170
171 func (r *Reporter) LogMaxima(logger logPrinter, memLimits map[string]int64) {
172         if r.lastCPUSample.hasData {
173                 logger.Printf("Total CPU usage was %f user and %f sys on %d CPUs",
174                         r.lastCPUSample.user, r.lastCPUSample.sys, r.lastCPUSample.cpus)
175         }
176         for disk, sample := range r.lastDiskIOSample {
177                 logger.Printf("Total disk I/O on %s was %d bytes written and %d bytes read",
178                         disk, sample.txBytes, sample.rxBytes)
179         }
180         if r.maxDiskSpaceSample.total > 0 {
181                 percentage := 100 * r.maxDiskSpaceSample.used / r.maxDiskSpaceSample.total
182                 logger.Printf("Maximum disk usage was %d%%, %d/%d bytes",
183                         percentage, r.maxDiskSpaceSample.used, r.maxDiskSpaceSample.total)
184         }
185         for _, statName := range memoryStats {
186                 value, ok := r.maxMemSample[memoryKey{statName: "total_" + statName}]
187                 if !ok {
188                         value, ok = r.maxMemSample[memoryKey{statName: statName}]
189                 }
190                 if ok {
191                         r.reportMemoryMax(logger, "container", statName, value, memLimits[statName])
192                 }
193         }
194         for ifname, sample := range r.lastNetSample {
195                 logger.Printf("Total network I/O on %s was %d bytes written and %d bytes read",
196                         ifname, sample.txBytes, sample.rxBytes)
197         }
198 }
199
200 func (r *Reporter) LogProcessMemMax(logger logPrinter) {
201         for memKey, value := range r.maxMemSample {
202                 if memKey.processName == "" {
203                         continue
204                 }
205                 r.reportMemoryMax(logger, memKey.processName, memKey.statName, value, 0)
206         }
207 }
208
209 func (r *Reporter) readAllOrWarn(in io.Reader) ([]byte, error) {
210         content, err := ioutil.ReadAll(in)
211         if err != nil {
212                 r.Logger.Printf("warning: %v", err)
213         }
214         return content, err
215 }
216
217 // Open the cgroup stats file in /sys/fs corresponding to the target
218 // cgroup, and return an io.ReadCloser. If no stats file is available,
219 // return nil.
220 //
221 // Log the file that was opened, if it isn't the same file opened on
222 // the last openStatFile for this stat.
223 //
224 // Log "not available" if no file is found and either this stat has
225 // been available in the past, or verbose==true.
226 //
227 // TODO: Instead of trying all options, choose a process in the
228 // container, and read /proc/PID/cgroup to determine the appropriate
229 // cgroup root for the given statgroup. (This will avoid falling back
230 // to host-level stats during container setup and teardown.)
231 func (r *Reporter) openStatFile(statgroup, stat string, verbose bool) (io.ReadCloser, error) {
232         var paths []string
233         if r.CID != "" {
234                 // Collect container's stats
235                 paths = []string{
236                         fmt.Sprintf("%s/%s/%s/%s/%s", r.CgroupRoot, statgroup, r.CgroupParent, r.CID, stat),
237                         fmt.Sprintf("%s/%s/%s/%s", r.CgroupRoot, r.CgroupParent, r.CID, stat),
238                 }
239         } else {
240                 // Collect this host's stats
241                 paths = []string{
242                         fmt.Sprintf("%s/%s/%s", r.CgroupRoot, statgroup, stat),
243                         fmt.Sprintf("%s/%s", r.CgroupRoot, stat),
244                 }
245         }
246         var path string
247         var file *os.File
248         var err error
249         for _, path = range paths {
250                 file, err = os.Open(path)
251                 if err == nil {
252                         break
253                 } else {
254                         path = ""
255                 }
256         }
257         if pathWas := r.reportedStatFile[stat]; pathWas != path {
258                 // Log whenever we start using a new/different cgroup
259                 // stat file for a given statistic. This typically
260                 // happens 1 to 3 times per statistic, depending on
261                 // whether we happen to collect stats [a] before any
262                 // processes have been created in the container and
263                 // [b] after all contained processes have exited.
264                 if path == "" && verbose {
265                         r.Logger.Printf("notice: stats not available: stat %s, statgroup %s, cid %s, parent %s, root %s\n", stat, statgroup, r.CID, r.CgroupParent, r.CgroupRoot)
266                 } else if pathWas != "" {
267                         r.Logger.Printf("notice: stats moved from %s to %s\n", r.reportedStatFile[stat], path)
268                 } else {
269                         r.Logger.Printf("notice: reading stats from %s\n", path)
270                 }
271                 r.reportedStatFile[stat] = path
272         }
273         return file, err
274 }
275
276 func (r *Reporter) getContainerNetStats() (io.Reader, error) {
277         procsFile, err := r.openStatFile("cpuacct", "cgroup.procs", true)
278         if err != nil {
279                 return nil, err
280         }
281         defer procsFile.Close()
282         reader := bufio.NewScanner(procsFile)
283         for reader.Scan() {
284                 taskPid := reader.Text()
285                 statsFilename := fmt.Sprintf("/proc/%s/net/dev", taskPid)
286                 stats, err := ioutil.ReadFile(statsFilename)
287                 if err != nil {
288                         r.Logger.Printf("notice: %v", err)
289                         continue
290                 }
291                 return strings.NewReader(string(stats)), nil
292         }
293         return nil, errors.New("Could not read stats for any proc in container")
294 }
295
296 type ioSample struct {
297         sampleTime time.Time
298         txBytes    int64
299         rxBytes    int64
300 }
301
302 func (r *Reporter) doBlkIOStats() {
303         c, err := r.openStatFile("blkio", "blkio.io_service_bytes", true)
304         if err != nil {
305                 return
306         }
307         defer c.Close()
308         b := bufio.NewScanner(c)
309         var sampleTime = time.Now()
310         newSamples := make(map[string]ioSample)
311         for b.Scan() {
312                 var device, op string
313                 var val int64
314                 if _, err := fmt.Sscanf(string(b.Text()), "%s %s %d", &device, &op, &val); err != nil {
315                         continue
316                 }
317                 var thisSample ioSample
318                 var ok bool
319                 if thisSample, ok = newSamples[device]; !ok {
320                         thisSample = ioSample{sampleTime, -1, -1}
321                 }
322                 switch op {
323                 case "Read":
324                         thisSample.rxBytes = val
325                 case "Write":
326                         thisSample.txBytes = val
327                 }
328                 newSamples[device] = thisSample
329         }
330         for dev, sample := range newSamples {
331                 if sample.txBytes < 0 || sample.rxBytes < 0 {
332                         continue
333                 }
334                 delta := ""
335                 if prev, ok := r.lastDiskIOSample[dev]; ok {
336                         delta = fmt.Sprintf(" -- interval %.4f seconds %d write %d read",
337                                 sample.sampleTime.Sub(prev.sampleTime).Seconds(),
338                                 sample.txBytes-prev.txBytes,
339                                 sample.rxBytes-prev.rxBytes)
340                 }
341                 r.Logger.Printf("blkio:%s %d write %d read%s\n", dev, sample.txBytes, sample.rxBytes, delta)
342                 r.lastDiskIOSample[dev] = sample
343         }
344 }
345
346 type memSample struct {
347         sampleTime time.Time
348         memStat    map[string]int64
349 }
350
351 func (r *Reporter) getMemSample() {
352         c, err := r.openStatFile("memory", "memory.stat", true)
353         if err != nil {
354                 return
355         }
356         defer c.Close()
357         b := bufio.NewScanner(c)
358         thisSample := memSample{time.Now(), make(map[string]int64)}
359         for b.Scan() {
360                 var stat string
361                 var val int64
362                 if _, err := fmt.Sscanf(string(b.Text()), "%s %d", &stat, &val); err != nil {
363                         continue
364                 }
365                 thisSample.memStat[stat] = val
366                 maxKey := memoryKey{statName: stat}
367                 if val > r.maxMemSample[maxKey] {
368                         r.maxMemSample[maxKey] = val
369                 }
370         }
371         r.lastMemSample = thisSample
372
373         if r.ThresholdLogger != nil {
374                 for statName, thresholds := range r.MemThresholds {
375                         statValue, ok := thisSample.memStat["total_"+statName]
376                         if !ok {
377                                 statValue, ok = thisSample.memStat[statName]
378                                 if !ok {
379                                         continue
380                                 }
381                         }
382                         var index int
383                         var statThreshold Threshold
384                         for index, statThreshold = range thresholds {
385                                 if statValue < statThreshold.threshold {
386                                         break
387                                 } else if statThreshold.percentage > 0 {
388                                         r.ThresholdLogger.Printf("Container using over %d%% of memory (%s %d/%d bytes)",
389                                                 statThreshold.percentage, statName, statValue, statThreshold.total)
390                                 } else {
391                                         r.ThresholdLogger.Printf("Container using over %d of memory (%s %s bytes)",
392                                                 statThreshold.threshold, statName, statValue)
393                                 }
394                         }
395                         r.MemThresholds[statName] = thresholds[index:]
396                 }
397         }
398 }
399
400 func (r *Reporter) reportMemSample() {
401         var outstat bytes.Buffer
402         for _, key := range memoryStats {
403                 // Use "total_X" stats (entire hierarchy) if enabled,
404                 // otherwise just the single cgroup -- see
405                 // https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt
406                 if val, ok := r.lastMemSample.memStat["total_"+key]; ok {
407                         fmt.Fprintf(&outstat, " %d %s", val, key)
408                 } else if val, ok := r.lastMemSample.memStat[key]; ok {
409                         fmt.Fprintf(&outstat, " %d %s", val, key)
410                 }
411         }
412         r.Logger.Printf("mem%s\n", outstat.String())
413 }
414
415 func (r *Reporter) doProcmemStats() {
416         if r.kernelPageSize == 0 {
417                 // assign "don't try again" value in case we give up
418                 // and return without assigning the real value
419                 r.kernelPageSize = -1
420                 buf, err := os.ReadFile("/proc/self/smaps")
421                 if err != nil {
422                         r.Logger.Printf("error reading /proc/self/smaps: %s", err)
423                         return
424                 }
425                 m := regexp.MustCompile(`\nKernelPageSize:\s*(\d+) kB\n`).FindSubmatch(buf)
426                 if len(m) != 2 {
427                         r.Logger.Printf("error parsing /proc/self/smaps: KernelPageSize not found")
428                         return
429                 }
430                 size, err := strconv.ParseInt(string(m[1]), 10, 64)
431                 if err != nil {
432                         r.Logger.Printf("error parsing /proc/self/smaps: KernelPageSize %q: %s", m[1], err)
433                         return
434                 }
435                 r.kernelPageSize = size * 1024
436         } else if r.kernelPageSize < 0 {
437                 // already failed to determine page size, don't keep
438                 // trying/logging
439                 return
440         }
441
442         r.reportPIDsMu.Lock()
443         defer r.reportPIDsMu.Unlock()
444         procnames := make([]string, 0, len(r.reportPIDs))
445         for name := range r.reportPIDs {
446                 procnames = append(procnames, name)
447         }
448         sort.Strings(procnames)
449         procmem := ""
450         for _, procname := range procnames {
451                 pid := r.reportPIDs[procname]
452                 buf, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
453                 if err != nil {
454                         continue
455                 }
456                 // If the executable name contains a ')' char,
457                 // /proc/$pid/stat will look like '1234 (exec name)) S
458                 // 123 ...' -- the last ')' is the end of the 2nd
459                 // field.
460                 paren := bytes.LastIndexByte(buf, ')')
461                 if paren < 0 {
462                         continue
463                 }
464                 fields := bytes.SplitN(buf[paren:], []byte{' '}, 24)
465                 if len(fields) < 24 {
466                         continue
467                 }
468                 // rss is the 24th field in .../stat, and fields[0]
469                 // here is the last char ')' of the 2nd field, so
470                 // rss is fields[22]
471                 rss, err := strconv.ParseInt(string(fields[22]), 10, 64)
472                 if err != nil {
473                         continue
474                 }
475                 value := rss * r.kernelPageSize
476                 procmem += fmt.Sprintf(" %d %s", value, procname)
477                 maxKey := memoryKey{pid, procname, "rss"}
478                 if value > r.maxMemSample[maxKey] {
479                         r.maxMemSample[maxKey] = value
480                 }
481         }
482         if procmem != "" {
483                 r.Logger.Printf("procmem%s\n", procmem)
484         }
485 }
486
487 func (r *Reporter) doNetworkStats() {
488         sampleTime := time.Now()
489         stats, err := r.getContainerNetStats()
490         if err != nil {
491                 return
492         }
493
494         scanner := bufio.NewScanner(stats)
495         for scanner.Scan() {
496                 var ifName string
497                 var rx, tx int64
498                 words := strings.Fields(scanner.Text())
499                 if len(words) != 17 {
500                         // Skip lines with wrong format
501                         continue
502                 }
503                 ifName = strings.TrimRight(words[0], ":")
504                 if ifName == "lo" || ifName == "" {
505                         // Skip loopback interface and lines with wrong format
506                         continue
507                 }
508                 if tx, err = strconv.ParseInt(words[9], 10, 64); err != nil {
509                         continue
510                 }
511                 if rx, err = strconv.ParseInt(words[1], 10, 64); err != nil {
512                         continue
513                 }
514                 nextSample := ioSample{}
515                 nextSample.sampleTime = sampleTime
516                 nextSample.txBytes = tx
517                 nextSample.rxBytes = rx
518                 var delta string
519                 if prev, ok := r.lastNetSample[ifName]; ok {
520                         interval := nextSample.sampleTime.Sub(prev.sampleTime).Seconds()
521                         delta = fmt.Sprintf(" -- interval %.4f seconds %d tx %d rx",
522                                 interval,
523                                 tx-prev.txBytes,
524                                 rx-prev.rxBytes)
525                 }
526                 r.Logger.Printf("net:%s %d tx %d rx%s\n", ifName, tx, rx, delta)
527                 r.lastNetSample[ifName] = nextSample
528         }
529 }
530
531 type diskSpaceSample struct {
532         hasData    bool
533         sampleTime time.Time
534         total      uint64
535         used       uint64
536         available  uint64
537 }
538
539 func (r *Reporter) doDiskSpaceStats() {
540         s := syscall.Statfs_t{}
541         err := syscall.Statfs(r.TempDir, &s)
542         if err != nil {
543                 return
544         }
545         bs := uint64(s.Bsize)
546         nextSample := diskSpaceSample{
547                 hasData:    true,
548                 sampleTime: time.Now(),
549                 total:      s.Blocks * bs,
550                 used:       (s.Blocks - s.Bfree) * bs,
551                 available:  s.Bavail * bs,
552         }
553         if nextSample.used > r.maxDiskSpaceSample.used {
554                 r.maxDiskSpaceSample = nextSample
555         }
556
557         var delta string
558         if r.lastDiskSpaceSample.hasData {
559                 prev := r.lastDiskSpaceSample
560                 interval := nextSample.sampleTime.Sub(prev.sampleTime).Seconds()
561                 delta = fmt.Sprintf(" -- interval %.4f seconds %d used",
562                         interval,
563                         int64(nextSample.used-prev.used))
564         }
565         r.Logger.Printf("statfs %d available %d used %d total%s\n",
566                 nextSample.available, nextSample.used, nextSample.total, delta)
567         r.lastDiskSpaceSample = nextSample
568 }
569
570 type cpuSample struct {
571         hasData    bool // to distinguish the zero value from real data
572         sampleTime time.Time
573         user       float64
574         sys        float64
575         cpus       int64
576 }
577
578 // Return the number of CPUs available in the container. Return 0 if
579 // we can't figure out the real number of CPUs.
580 func (r *Reporter) getCPUCount() int64 {
581         cpusetFile, err := r.openStatFile("cpuset", "cpuset.cpus", true)
582         if err != nil {
583                 return 0
584         }
585         defer cpusetFile.Close()
586         b, err := r.readAllOrWarn(cpusetFile)
587         if err != nil {
588                 return 0
589         }
590         sp := strings.Split(string(b), ",")
591         cpus := int64(0)
592         for _, v := range sp {
593                 var min, max int64
594                 n, _ := fmt.Sscanf(v, "%d-%d", &min, &max)
595                 if n == 2 {
596                         cpus += (max - min) + 1
597                 } else {
598                         cpus++
599                 }
600         }
601         return cpus
602 }
603
604 func (r *Reporter) doCPUStats() {
605         statFile, err := r.openStatFile("cpuacct", "cpuacct.stat", true)
606         if err != nil {
607                 return
608         }
609         defer statFile.Close()
610         b, err := r.readAllOrWarn(statFile)
611         if err != nil {
612                 return
613         }
614
615         var userTicks, sysTicks int64
616         fmt.Sscanf(string(b), "user %d\nsystem %d", &userTicks, &sysTicks)
617         userHz := float64(100)
618         nextSample := cpuSample{
619                 hasData:    true,
620                 sampleTime: time.Now(),
621                 user:       float64(userTicks) / userHz,
622                 sys:        float64(sysTicks) / userHz,
623                 cpus:       r.getCPUCount(),
624         }
625
626         delta := ""
627         if r.lastCPUSample.hasData {
628                 delta = fmt.Sprintf(" -- interval %.4f seconds %.4f user %.4f sys",
629                         nextSample.sampleTime.Sub(r.lastCPUSample.sampleTime).Seconds(),
630                         nextSample.user-r.lastCPUSample.user,
631                         nextSample.sys-r.lastCPUSample.sys)
632         }
633         r.Logger.Printf("cpu %.4f user %.4f sys %d cpus%s\n",
634                 nextSample.user, nextSample.sys, nextSample.cpus, delta)
635         r.lastCPUSample = nextSample
636 }
637
638 func (r *Reporter) doAllStats() {
639         r.reportMemSample()
640         r.doProcmemStats()
641         r.doCPUStats()
642         r.doBlkIOStats()
643         r.doNetworkStats()
644         r.doDiskSpaceStats()
645 }
646
647 // Report stats periodically until we learn (via r.done) that someone
648 // called Stop.
649 func (r *Reporter) run() {
650         defer close(r.flushed)
651
652         r.maxMemSample = make(map[memoryKey]int64)
653         r.reportedStatFile = make(map[string]string)
654
655         if !r.waitForCIDFile() || !r.waitForCgroup() {
656                 return
657         }
658
659         r.lastNetSample = make(map[string]ioSample)
660         r.lastDiskIOSample = make(map[string]ioSample)
661
662         if len(r.TempDir) == 0 {
663                 // Temporary dir not provided, try to get it from the environment.
664                 r.TempDir = os.Getenv("TMPDIR")
665         }
666         if len(r.TempDir) > 0 {
667                 r.Logger.Printf("notice: monitoring temp dir %s\n", r.TempDir)
668         }
669
670         r.getMemSample()
671         r.doAllStats()
672
673         memTicker := time.NewTicker(time.Second)
674         mainTicker := time.NewTicker(r.PollPeriod)
675         for {
676                 select {
677                 case <-r.done:
678                         return
679                 case <-memTicker.C:
680                         r.getMemSample()
681                 case <-mainTicker.C:
682                         r.doAllStats()
683                 }
684         }
685 }
686
687 // If CID is empty, wait for it to appear in CIDFile. Return true if
688 // we get it before we learn (via r.done) that someone called Stop.
689 func (r *Reporter) waitForCIDFile() bool {
690         if r.CID != "" || r.CIDFile == "" {
691                 return true
692         }
693
694         ticker := time.NewTicker(100 * time.Millisecond)
695         defer ticker.Stop()
696         for {
697                 cid, err := ioutil.ReadFile(r.CIDFile)
698                 if err == nil && len(cid) > 0 {
699                         r.CID = string(cid)
700                         return true
701                 }
702                 select {
703                 case <-ticker.C:
704                 case <-r.done:
705                         r.Logger.Printf("warning: CID never appeared in %+q: %v", r.CIDFile, err)
706                         return false
707                 }
708         }
709 }
710
711 // Wait for the cgroup stats files to appear in cgroup_root. Return
712 // true if they appear before r.done indicates someone called Stop. If
713 // they don't appear within one poll interval, log a warning and keep
714 // waiting.
715 func (r *Reporter) waitForCgroup() bool {
716         ticker := time.NewTicker(100 * time.Millisecond)
717         defer ticker.Stop()
718         warningTimer := time.After(r.PollPeriod)
719         for {
720                 c, err := r.openStatFile("cpuacct", "cgroup.procs", false)
721                 if err == nil {
722                         c.Close()
723                         return true
724                 }
725                 select {
726                 case <-ticker.C:
727                 case <-warningTimer:
728                         r.Logger.Printf("warning: cgroup stats files have not appeared after %v (config error?) -- still waiting...", r.PollPeriod)
729                 case <-r.done:
730                         r.Logger.Printf("warning: cgroup stats files never appeared for %v", r.CID)
731                         return false
732                 }
733         }
734 }