lib/discovery: Export information on advertisements and scans.

- The discovery implementation exports all the AdInfos being advertised
- The BLE plugin exports the set of devices it has scanned
  (Temporarily, I'm going to try to move this up to the discovery
  implementation instead)

Change-Id: I8e9f5ffdf80f4191d3e9c24a893e71f5e6267b11
diff --git a/lib/discovery/advertise.go b/lib/discovery/advertise.go
index 342c614..8b20b50 100644
--- a/lib/discovery/advertise.go
+++ b/lib/discovery/advertise.go
@@ -5,12 +5,15 @@
 package discovery
 
 import (
+	"fmt"
 	"sync"
 	"time"
 
 	"v.io/v23/context"
 	"v.io/v23/discovery"
+	"v.io/v23/naming"
 	"v.io/v23/security"
+	"v.io/x/ref/lib/stats"
 )
 
 const (
@@ -114,6 +117,8 @@
 }
 
 func (d *idiscovery) startAdvertising(ctx *context.T, adinfo *AdInfo) (func(), error) {
+	statName := naming.Join(d.statsPrefix, "ad", adinfo.Ad.Id.String())
+	stats.NewStringFunc(statName, func() string { return fmt.Sprint(*adinfo) })
 	ctx, cancel := context.WithCancel(ctx)
 	var wg sync.WaitGroup
 	for _, plugin := range d.plugins {
@@ -124,6 +129,7 @@
 	}
 
 	stop := func() {
+		stats.Delete(statName)
 		cancel()
 		wg.Wait()
 	}
diff --git a/lib/discovery/discovery.go b/lib/discovery/discovery.go
index 1c024f4..81a2192 100644
--- a/lib/discovery/discovery.go
+++ b/lib/discovery/discovery.go
@@ -5,10 +5,12 @@
 package discovery
 
 import (
+	"fmt"
 	"sync"
 
 	"v.io/v23/context"
 	"v.io/v23/discovery"
+	"v.io/v23/naming"
 )
 
 type idiscovery struct {
@@ -26,8 +28,15 @@
 	adStopTrigger *Trigger
 
 	dirServer *dirServer
+
+	statsPrefix string
 }
 
+var (
+	statsMu  sync.Mutex
+	statsIdx int
+)
+
 type sessionId uint64
 
 type adSubtask struct {
@@ -91,12 +100,17 @@
 	if len(plugins) == 0 {
 		return nil, NewErrNoDiscoveryPlugin(ctx)
 	}
+	statsMu.Lock()
+	statsPrefix := naming.Join("discovery", fmt.Sprint(statsIdx))
+	statsIdx++
+	statsMu.Unlock()
 	d := &idiscovery{
 		plugins:       make([]Plugin, len(plugins)),
 		tasks:         make(map[*context.T]func()),
 		adSessions:    make(map[discovery.AdId]sessionId),
 		adSubtasks:    make(map[discovery.AdId]*adSubtask),
 		adStopTrigger: NewTrigger(),
+		statsPrefix:   statsPrefix,
 	}
 	copy(d.plugins, plugins)
 
diff --git a/lib/discovery/plugins/ble/ble.go b/lib/discovery/plugins/ble/ble.go
index dad1926..e08ecb7 100644
--- a/lib/discovery/plugins/ble/ble.go
+++ b/lib/discovery/plugins/ble/ble.go
@@ -5,6 +5,7 @@
 package ble
 
 import (
+	"bytes"
 	"fmt"
 	"runtime"
 	"sync"
@@ -12,6 +13,7 @@
 
 	"v.io/v23/context"
 	"v.io/v23/discovery"
+	"v.io/v23/naming"
 	idiscovery "v.io/x/ref/lib/discovery"
 	"v.io/x/ref/lib/stats"
 )
@@ -30,8 +32,8 @@
 type plugin struct {
 	advertiser *advertiser
 	scanner    *scanner
-
-	adStopper *idiscovery.Trigger
+	adStopper  *idiscovery.Trigger
+	statPrefix string
 }
 
 func (p *plugin) Advertise(ctx *context.T, adinfo *idiscovery.AdInfo, done func()) (err error) {
@@ -55,17 +57,38 @@
 		defer p.scanner.removeListener(interfaceName, listener)
 
 		seen := make(map[discovery.AdId]*idiscovery.AdInfo)
+
+		// TODO(ashankar,jhahn): To prevent plugins from stepping over
+		// each other (e.g., a Lost even from one undoing a Found event
+		// from another), the discovery implementation that uses
+		// plugins should be made aware of the plugin that sent the event.
+		// In that case, perhaps these stats should also be exported there,
+		// rather than in each plugin implementation?
+		stat := naming.Join(p.statPrefix, "seen")
+		var seenMu sync.Mutex // Safety between this goroutine and stats
+		stats.NewStringFunc(stat, func() string {
+			seenMu.Lock()
+			defer seenMu.Unlock()
+			buf := new(bytes.Buffer)
+			for k, v := range seen {
+				fmt.Fprintf(buf, "%s: %v\n\n", k, *v)
+			}
+			return buf.String()
+		})
+		defer stats.Delete(stat)
 		for {
 			select {
 			case adinfo := <-listener:
 				if adinfo.Lost {
+					seenMu.Lock()
 					delete(seen, adinfo.Ad.Id)
+					seenMu.Unlock()
+				} else if prev := seen[adinfo.Ad.Id]; prev != nil && (prev.Hash == adinfo.Hash || prev.TimestampNs >= adinfo.TimestampNs) {
+					continue
 				} else {
-					prev := seen[adinfo.Ad.Id]
-					if prev != nil && (prev.Hash == adinfo.Hash || prev.TimestampNs >= adinfo.TimestampNs) {
-						continue
-					}
+					seenMu.Lock()
 					seen[adinfo.Ad.Id] = adinfo
+					seenMu.Unlock()
 				}
 				copied := *adinfo
 				select {
@@ -98,9 +121,9 @@
 		return nil, err
 	}
 	statMu.Lock()
-	statName := fmt.Sprintf("discovery/ble/driver/%d", statIdx)
+	statPrefix := naming.Join("discovery", "ble", fmt.Sprint(statIdx))
 	statIdx++
-	stats.NewStringFunc(statName, func() string {
+	stats.NewStringFunc(naming.Join(statPrefix, "driver"), func() string {
 		return driver.DebugString()
 	})
 	statMu.Unlock()
@@ -108,7 +131,8 @@
 		advertiser: newAdvertiser(ctx, driver),
 		scanner:    newScanner(ctx, driver, ttl),
 		adStopper:  idiscovery.NewTrigger(),
+		statPrefix: statPrefix,
 	}
-	runtime.SetFinalizer(p, func(p *plugin) { stats.Delete(statName) })
+	runtime.SetFinalizer(p, func(p *plugin) { stats.Delete(statPrefix) })
 	return p, nil
 }