github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/impatient_debug_dumper.go (about)

     1  package libkbfs
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"context"
     7  	"encoding/base64"
     8  	"fmt"
     9  	"runtime/pprof"
    10  	"sync"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"github.com/keybase/client/go/logger"
    15  	"golang.org/x/time/rate"
    16  )
    17  
    18  type ctxTimeTracker struct {
    19  	ctx       context.Context
    20  	expiresAt time.Time
    21  
    22  	done int32
    23  }
    24  
    25  func (c *ctxTimeTracker) markDone() {
    26  	atomic.StoreInt32(&c.done, 1)
    27  }
    28  
    29  func (c *ctxTimeTracker) isDone() bool {
    30  	return atomic.LoadInt32(&c.done) == 1
    31  }
    32  
    33  func (c *ctxTimeTracker) isExpired(clock Clock) bool {
    34  	return clock.Now().After(c.expiresAt)
    35  }
    36  
    37  type ctxTimeTrackerListNode struct {
    38  	tracker *ctxTimeTracker
    39  	next    *ctxTimeTrackerListNode
    40  }
    41  
    42  type ctxTimeTrackerList struct {
    43  	front *ctxTimeTrackerListNode
    44  	tail  *ctxTimeTrackerListNode
    45  }
    46  
    47  func (c *ctxTimeTrackerList) peekFront() (t *ctxTimeTracker) {
    48  	if c.front == nil {
    49  		return nil
    50  	}
    51  	return c.front.tracker
    52  }
    53  
    54  func (c *ctxTimeTrackerList) popFront() (t *ctxTimeTracker) {
    55  	if c.front == nil {
    56  		return nil
    57  	}
    58  	t = c.front.tracker
    59  	c.front = c.front.next
    60  	if c.front == nil {
    61  		// last one!
    62  		c.tail = nil
    63  	}
    64  	return t
    65  }
    66  
    67  func (c *ctxTimeTrackerList) append(t *ctxTimeTracker) {
    68  	newNode := &ctxTimeTrackerListNode{
    69  		tracker: t,
    70  	}
    71  	if c.tail != nil {
    72  		c.tail.next = newNode
    73  	}
    74  	c.tail = newNode
    75  	if c.front == nil {
    76  		c.front = newNode
    77  	}
    78  }
    79  
    80  // ImpatientDebugDumper dumps all running goroutines if an operation takes
    81  // longer than a preset value. User of this type should call Begin() with a
    82  // context associated with an operation, and call the returned function when
    83  // the operation is done. If the operation finishes within the preset duration,
    84  // nothing is dumped into log. Despite being impatient, it tries not to pollute
    85  // the log too much by rate limit goroutine dumping based on
    86  // impatientDebugDumperDumpMinInterval (at most 1 per minute).
    87  type ImpatientDebugDumper struct {
    88  	config Config
    89  	log    logger.Logger
    90  	dumpIn time.Duration
    91  
    92  	ticker         *time.Ticker
    93  	limiter        *rate.Limiter
    94  	shutdownFunc   func()
    95  	shutdownDoneCh chan struct{}
    96  
    97  	lock                         sync.Mutex
    98  	chronologicalTimeTrackerList *ctxTimeTrackerList
    99  }
   100  
   101  const impatientDebugDumperCheckInterval = time.Second
   102  const impatientDebugDumperDumpMinInterval = time.Minute // 1 dump per min max
   103  
   104  // NewImpatientDebugDumper creates a new *ImpatientDebugDumper, which logs with
   105  // a logger made by config.MakeLogger("IGD"), and dumps goroutines if an
   106  // operation takes longer than dumpIn.
   107  func NewImpatientDebugDumper(config Config, dumpIn time.Duration) *ImpatientDebugDumper {
   108  	ctx, cancel := context.WithCancel(context.Background())
   109  	d := &ImpatientDebugDumper{
   110  		config: config,
   111  		log:    config.MakeLogger("IGD"),
   112  		dumpIn: dumpIn,
   113  		ticker: time.NewTicker(impatientDebugDumperCheckInterval),
   114  		limiter: rate.NewLimiter(
   115  			rate.Every(impatientDebugDumperDumpMinInterval), 1),
   116  		shutdownFunc:                 cancel,
   117  		shutdownDoneCh:               make(chan struct{}),
   118  		chronologicalTimeTrackerList: &ctxTimeTrackerList{},
   119  	}
   120  	go d.dumpLoop(ctx.Done())
   121  	return d
   122  }
   123  
   124  // NewImpatientDebugDumperForForcedDumps creates a new
   125  // *ImpatientDebugDumper, just as above, though without launching any
   126  // background goroutines or allowing the user to begin any
   127  // time-tracked tasks.  `ForceDump` is the only way to obtain a dump
   128  // from a dumper constructed with this function.
   129  func NewImpatientDebugDumperForForcedDumps(config Config) *ImpatientDebugDumper {
   130  	return &ImpatientDebugDumper{
   131  		config: config,
   132  		log:    config.MakeLogger("IGD"),
   133  		limiter: rate.NewLimiter(
   134  			rate.Every(impatientDebugDumperDumpMinInterval), 1),
   135  	}
   136  }
   137  
   138  func (d *ImpatientDebugDumper) dump(ctx context.Context) {
   139  	if !d.limiter.Allow() {
   140  		// Use a limiter to avoid dumping too much into log accidently.
   141  		return
   142  	}
   143  	buf := &bytes.Buffer{}
   144  	base64er := base64.NewEncoder(base64.StdEncoding, buf)
   145  	gzipper := gzip.NewWriter(base64er)
   146  	for _, p := range pprof.Profiles() {
   147  		fmt.Fprintf(gzipper,
   148  			"\n======== START Profile: %s ========\n\n", p.Name())
   149  		_ = p.WriteTo(gzipper, 2)
   150  		fmt.Fprintf(gzipper,
   151  			"\n======== END   Profile: %s ========\n\n", p.Name())
   152  	}
   153  	gzipper.Close()
   154  	base64er.Close()
   155  	d.log.CDebugf(ctx,
   156  		"Operation exceeded max wait time. dump>gzip>base64: %q "+
   157  			"Pipe the quoted content into ` | base64 -d | gzip -d ` "+
   158  			"to read as a Homosapien.", buf.String())
   159  }
   160  
   161  // ForceDump dumps all debug info to the log, if it hasn't done so
   162  // recently according to the rate-limiter.
   163  func (d *ImpatientDebugDumper) ForceDump(ctx context.Context) {
   164  	d.dump(ctx)
   165  }
   166  
   167  func (d *ImpatientDebugDumper) dumpTick() {
   168  	d.lock.Lock()
   169  	defer d.lock.Unlock()
   170  	for {
   171  		// In each iteration we deal with the front of list:
   172  		//  1) If list is empty, we just return and wait for the next tick;
   173  		//  2) If front is done, pop front from the list, and continue into
   174  		//     next iteration to check next one;
   175  		//  3) If front is not done but expired, dump routines (if rate limiter
   176  		//     permits), pop front from the list, and continue into next
   177  		//     iteration to check next one;
   178  		//  4) If front is not done yet nor expired, just return and wait for
   179  		//     next tick when we check again.
   180  		//
   181  		// Since we either move on or return, there's no risk of infinite
   182  		// looping here.
   183  		t := d.chronologicalTimeTrackerList.peekFront()
   184  		if t == nil {
   185  			return
   186  		}
   187  		if t.isDone() {
   188  			// This operation is done, so just move on.
   189  			d.chronologicalTimeTrackerList.popFront()
   190  			continue
   191  		}
   192  		if t.isExpired(d.config.Clock()) {
   193  			// This operation isn't done, and it has expired. So dump debug
   194  			// information and move on.
   195  			d.dump(t.ctx)
   196  			d.chronologicalTimeTrackerList.popFront()
   197  			continue
   198  		}
   199  		// This operation isn't done yet, but it also hasn't expired. So
   200  		// just return and wait for next tick and check again.
   201  		return
   202  	}
   203  }
   204  
   205  func (d *ImpatientDebugDumper) dumpLoop(shutdownCh <-chan struct{}) {
   206  	defer close(d.shutdownDoneCh)
   207  	for {
   208  		select {
   209  		case <-d.ticker.C:
   210  			d.dumpTick()
   211  		case <-shutdownCh:
   212  			d.ticker.Stop()
   213  			d.log.Debug("shutdown")
   214  			return
   215  		}
   216  	}
   217  }
   218  
   219  // Begin causes d to start tracking time for ctx. The returned function should
   220  // be called once the associated operation is done, likely in a defer
   221  // statement.
   222  func (d *ImpatientDebugDumper) Begin(ctx context.Context) (done func()) {
   223  	if d.chronologicalTimeTrackerList == nil {
   224  		return nil
   225  	}
   226  	tracker := &ctxTimeTracker{
   227  		ctx:       ctx,
   228  		expiresAt: d.config.Clock().Now().Add(d.dumpIn),
   229  	}
   230  	d.lock.Lock()
   231  	defer d.lock.Unlock()
   232  	d.chronologicalTimeTrackerList.append(tracker)
   233  	return tracker.markDone
   234  }
   235  
   236  // Shutdown shuts down d idempotently.
   237  func (d *ImpatientDebugDumper) Shutdown() {
   238  	if d.shutdownFunc != nil {
   239  		d.shutdownFunc()
   240  		<-d.shutdownDoneCh
   241  	}
   242  }