@@ -13,7 +13,6 @@ import (
1313 "net/http"
1414 "os"
1515 "path/filepath"
16- "strings"
1716 "sync"
1817 "sync/atomic"
1918 "time"
@@ -60,6 +59,7 @@ type Client struct {
6059 httpClient * http.Client
6160
6261 crawlerConcurrency int
62+ readyQueueSize int
6363 wg sync.WaitGroup
6464}
6565
@@ -79,6 +79,16 @@ func WithClientHelloID(id tls.ClientHelloID) Option {
7979}
8080func WithCrawlerConcurrency (n int ) Option { return func (c * Client ) { c .crawlerConcurrency = n } }
8181
82+ // WithReadyQueueSize sets the capacity of the ready-fronts channel.
83+ // Smaller values save memory on constrained devices. Default is 500.
84+ func WithReadyQueueSize (n int ) Option { return func (c * Client ) { c .readyQueueSize = n } }
85+
86+ // WithCacheSaveInterval sets how often dirty cache state is flushed to disk.
87+ // Longer intervals reduce I/O on flash storage (e.g. Android). Default is 5s.
88+ func WithCacheSaveInterval (d time.Duration ) Option {
89+ return func (c * Client ) { c .cacheSaveInterval = d }
90+ }
91+
8292// WithCacheFile is a convenience option that sets a FileCache at the given path.
8393func WithCacheFile (path string ) Option {
8494 return func (c * Client ) { c .cache = & FileCache {Path : path } }
@@ -97,7 +107,7 @@ func New(ctx context.Context, config *Config, options ...Option) (*Client, error
97107 ctx : innerCtx ,
98108 cancel : cancel ,
99109 log : slog .Default (),
100- pool : newFrontPool (),
110+ pool : nil , // created after options are applied
101111 providers : make (map [string ]* Provider ),
102112 dialer : NetDialer {},
103113 clientHelloID : tls .HelloChrome_131 ,
@@ -121,6 +131,9 @@ func New(ctx context.Context, config *Config, options ...Option) (*Client, error
121131 c .crawlerConcurrency = 1
122132 }
123133
134+ // Create pool after options are applied so readyQueueSize can be set
135+ c .pool = newFrontPool (c .readyQueueSize )
136+
124137 if err := c .applyConfig (config ); err != nil {
125138 cancel ()
126139 return nil , fmt .Errorf ("invalid config: %w" , err )
@@ -230,15 +243,26 @@ func (c *Client) notifyCacheDirty() {
230243func (c * Client ) crawler () {
231244 defer c .wg .Done ()
232245
246+ // Use a reusable timer instead of time.After to avoid leaking timers.
247+ // time.After creates a new timer each iteration that isn't GC'd until it
248+ // fires — on memory-constrained devices this creates mounting GC pressure.
249+ // Initialize stopped and drain to avoid an immediate spurious wake.
250+ timer := time .NewTimer (0 )
251+ if ! timer .Stop () {
252+ <- timer .C
253+ }
254+ defer timer .Stop ()
255+
233256 for {
234257 if c .pool .readyCount () < 2 {
235258 c .crawlAllFronts ()
236259 }
237260
261+ timer .Reset (time .Duration (6 + rand .IntN (7 )) * time .Second )
238262 select {
239263 case <- c .ctx .Done ():
240264 return
241- case <- time . After ( time . Duration ( 6 + rand . IntN ( 7 )) * time . Second ) :
265+ case <- timer . C :
242266 }
243267 }
244268}
@@ -300,16 +324,19 @@ func (c *Client) vetFront(f *front) bool {
300324 return c .verifyWithPost (result .conn , provider .TestURL )
301325}
302326
327+ // vetBody is reused across vet requests to avoid per-call allocation.
328+ var vetBody = []byte ("a" )
329+
303330func (c * Client ) verifyWithPost (conn net.Conn , testURL string ) bool {
304331 tr := newConnTransport (conn , true )
305- client := & http.Client {Transport : tr }
306- req , err := http .NewRequest (http .MethodPost , testURL , strings .NewReader ("a" ))
332+ req , err := http .NewRequest (http .MethodPost , testURL , bytes .NewReader (vetBody ))
307333 if err != nil {
308334 c .log .Debug ("Error creating vet request" , "error" , err )
309335 return false
310336 }
337+ req .URL .Scheme = "http" // TLS already established on conn
311338 req .Header .Set ("Content-Type" , "application/json" )
312- resp , err := client . Do (req )
339+ resp , err := tr . RoundTrip (req )
313340 if err != nil {
314341 c .log .Debug ("Error vetting front" , "error" , err , "url" , testURL )
315342 return false
@@ -328,16 +355,20 @@ func (c *Client) verifyWithPost(conn net.Conn, testURL string) bool {
328355func (c * Client ) cacheSaver () {
329356 defer c .wg .Done ()
330357
358+ timer := time .NewTimer (c .cacheSaveInterval )
359+ defer timer .Stop ()
360+
331361 for {
332362 select {
333363 case <- c .ctx .Done ():
334364 return
335- case <- time . After ( c . cacheSaveInterval ) :
365+ case <- timer . C :
336366 select {
337367 case <- c .cacheDirty :
338368 c .saveCache ()
339369 default :
340370 }
371+ timer .Reset (c .cacheSaveInterval )
341372 }
342373 }
343374}
0 commit comments