net: cache IPv6 zone information for applications using IPv6 link-local address

This change reduces the overhead of calling routing information per IPv6
link-local datagram read by caching IPv6 addressing scope zone
information.

Fixes #15237.

name                    old time/op    new time/op    delta
UDP6LinkLocalUnicast-8    64.9µs ± 0%    18.6µs ± 0%  -71.30%

name                    old alloc/op   new alloc/op   delta
UDP6LinkLocalUnicast-8    11.2kB ± 0%     0.2kB ± 0%  -98.42%

name                    old allocs/op  new allocs/op  delta
UDP6LinkLocalUnicast-8       101 ± 0%         3 ± 0%  -97.03%

Change-Id: I5ae2ef5058df1028bbb7f4ab32b13edfb330c3a7
Reviewed-on: https://go-review.googlesource.com/21952
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/src/net/interface.go b/src/net/interface.go
index c99f8fd..52b857c 100644
--- a/src/net/interface.go
+++ b/src/net/interface.go
@@ -4,7 +4,11 @@
 
 package net
 
-import "errors"
+import (
+	"errors"
+	"sync"
+	"time"
+)
 
 var (
 	errInvalidInterface         = errors.New("invalid network interface")
@@ -88,9 +92,12 @@
 func Interfaces() ([]Interface, error) {
 	ift, err := interfaceTable(0)
 	if err != nil {
-		err = &OpError{Op: "route", Net: "ip+net", Source: nil, Addr: nil, Err: err}
+		return nil, &OpError{Op: "route", Net: "ip+net", Source: nil, Addr: nil, Err: err}
 	}
-	return ift, err
+	if len(ift) != 0 {
+		zoneCache.update(ift)
+	}
+	return ift, nil
 }
 
 // InterfaceAddrs returns a list of the system's network interface
@@ -137,6 +144,9 @@
 	if err != nil {
 		return nil, &OpError{Op: "route", Net: "ip+net", Source: nil, Addr: nil, Err: err}
 	}
+	if len(ift) != 0 {
+		zoneCache.update(ift)
+	}
 	for _, ifi := range ift {
 		if name == ifi.Name {
 			return &ifi, nil
@@ -144,3 +154,68 @@
 	}
 	return nil, &OpError{Op: "route", Net: "ip+net", Source: nil, Addr: nil, Err: errNoSuchInterface}
 }
+
+// An ipv6ZoneCache represents a cache holding partial network
+// interface information. It is used for reducing the cost of IPv6
+// addressing scope zone resolution.
+type ipv6ZoneCache struct {
+	sync.RWMutex                // guard the following
+	lastFetched  time.Time      // last time routing information was fetched
+	toIndex      map[string]int // interface name to its index
+	toName       map[int]string // interface index to its name
+}
+
+var zoneCache = ipv6ZoneCache{
+	toIndex: make(map[string]int),
+	toName:  make(map[int]string),
+}
+
+func (zc *ipv6ZoneCache) update(ift []Interface) {
+	zc.Lock()
+	defer zc.Unlock()
+	now := time.Now()
+	if zc.lastFetched.After(now.Add(-60 * time.Second)) {
+		return
+	}
+	zc.lastFetched = now
+	if len(ift) == 0 {
+		var err error
+		if ift, err = interfaceTable(0); err != nil {
+			return
+		}
+	}
+	zc.toIndex = make(map[string]int, len(ift))
+	zc.toName = make(map[int]string, len(ift))
+	for _, ifi := range ift {
+		zc.toIndex[ifi.Name] = ifi.Index
+		zc.toName[ifi.Index] = ifi.Name
+	}
+}
+
+func zoneToString(zone int) string {
+	if zone == 0 {
+		return ""
+	}
+	zoneCache.update(nil)
+	zoneCache.RLock()
+	defer zoneCache.RUnlock()
+	name, ok := zoneCache.toName[zone]
+	if !ok {
+		name = uitoa(uint(zone))
+	}
+	return name
+}
+
+func zoneToInt(zone string) int {
+	if zone == "" {
+		return 0
+	}
+	zoneCache.update(nil)
+	zoneCache.RLock()
+	defer zoneCache.RUnlock()
+	index, ok := zoneCache.toIndex[zone]
+	if !ok {
+		index, _, _ = dtoi(zone, 0)
+	}
+	return index
+}