tailscale/portlist/poller.go
Brad Fitzpatrick 46ce80758d portlist: update some internals to use append-style APIs
In prep for reducing garbage, being able to reuse memory.  So far this
doesn't actually reuse much. This is just changing signatures around.

But some improvement in any case:

    bradfitz@tsdev:~/src/tailscale.com$ ~/go/bin/benchstat before after
    name       old time/op    new time/op    delta
    GetList-8    11.8ms ± 9%     9.9ms ± 3%  -15.98%  (p=0.000 n=10+10)

    name       old alloc/op   new alloc/op   delta
    GetList-8    99.5kB ± 2%    91.9kB ± 0%   -7.62%  (p=0.000 n=9+9)

    name       old allocs/op  new allocs/op  delta
    GetList-8     3.05k ± 1%     2.93k ± 0%   -3.83%  (p=0.000 n=8+9)

More later, once parsers can reuse strings from previous parses.

Updates #5958

Change-Id: I76cd5048246dd24d11c4e263d8bb8041747fb2b0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-10-21 22:26:37 -07:00

105 lines
2.1 KiB
Go

// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portlist
import (
"context"
"errors"
"time"
"tailscale.com/version"
)
// Poller scans the systems for listening ports periodically and sends
// the results to C.
type Poller struct {
// C received the list of ports periodically. It's closed when
// Run completes, after which Err can be checked.
C <-chan List
// Err is the error from the final GetList call. It is only
// valid to read once C has been closed. Err is nil if Close
// is called or the context is canceled.
Err error
// scatch is memory for Poller.getList to reuse between calls.
scratch []Port
c chan List // the unconstrained version of the exported C above
quitCh chan struct{} // close this to force exit
prev List // most recent data
}
// NewPoller returns a new portlist Poller. It returns an error
// if the portlist couldn't be obtained.
func NewPoller() (*Poller, error) {
if version.OS() == "iOS" {
return nil, errors.New("not available on iOS")
}
p := &Poller{
c: make(chan List),
quitCh: make(chan struct{}),
}
p.C = p.c
// Do one initial poll synchronously so we can return an error
// early.
var err error
p.prev, err = p.getList()
if err != nil {
return nil, err
}
return p, nil
}
func (p *Poller) Close() error {
select {
case <-p.quitCh:
return nil
default:
}
close(p.quitCh)
<-p.C
return nil
}
// Run runs the Poller periodically until either the context
// is done, or the Close is called.
func (p *Poller) Run(ctx context.Context) error {
defer close(p.c)
tick := time.NewTicker(pollInterval)
defer tick.Stop()
// Send out the pre-generated initial value
p.c <- p.prev
for {
select {
case <-tick.C:
pl, err := p.getList()
if err != nil {
p.Err = err
return err
}
if pl.sameInodes(p.prev) {
continue
}
p.prev = pl
select {
case p.c <- pl:
case <-ctx.Done():
return ctx.Err()
case <-p.quitCh:
return nil
}
case <-ctx.Done():
return ctx.Err()
case <-p.quitCh:
return nil
}
}
}