[symbolize] Check modtime on ids.txt

This change checks the mod time on the ids.txt file
prior to reading the file and thus prior to performing
verificiation of the ids.txt. Coupled with caching this
reduces the primary cost of the symbolize to be parsing (30-40%, 30% regex alone),
lock contention (15%). This represents something like a 1000x
speed up. Further optimization seems much harder. Increasing
go channel buffer sizes should give a 25%-50% improvement.

Change-Id: I0da1a05c7e31afc68d8ddc2eecd0acf42ae59961
diff --git a/symbolize/mock_elf.go b/symbolize/mock_elf.go
index 1bc0b32..568a5da 100644
--- a/symbolize/mock_elf.go
+++ b/symbolize/mock_elf.go
@@ -5,6 +5,8 @@
 package symbolize
 
 import (
+	"time"
+
 	"fuchsia.googlesource.com/tools/elflib"
 )
 
@@ -21,3 +23,8 @@
 func (m mockSource) getBinaries() ([]elflib.BinaryFileRef, error) {
 	return []elflib.BinaryFileRef(m), nil
 }
+
+func (m mockSource) getModTime() (*time.Time, error) {
+	out := time.Unix(1, 0)
+	return &out, nil
+}
diff --git a/symbolize/repo.go b/symbolize/repo.go
index ee0c92b..3a77028 100644
--- a/symbolize/repo.go
+++ b/symbolize/repo.go
@@ -8,6 +8,7 @@
 	"fmt"
 	"os"
 	"sync"
+	"time"
 
 	"fuchsia.googlesource.com/tools/elflib"
 )
@@ -22,6 +23,7 @@
 type BinaryFileSource interface {
 	// Extracts the set of binaries from this source.
 	getBinaries() ([]elflib.BinaryFileRef, error)
+	getModTime() (*time.Time, error)
 }
 
 // idsSource is a BinaryFileSource parsed from ids.txt
@@ -46,15 +48,29 @@
 	return elflib.ReadIDsFile(file)
 }
 
+func (i *idsSource) getModTime() (*time.Time, error) {
+	info, err := os.Stat(i.pathToIDs)
+	if err != nil {
+		return nil, err
+	}
+	out := info.ModTime()
+	return &out, nil
+}
+
 type buildInfo struct {
 	filepath string
 	buildID  string
 }
 
+type modTimeSource struct {
+	BinaryFileSource
+	modTime time.Time
+}
+
 // SymbolizerRepo keeps track of build objects and source files used in those build objects.
 type SymbolizerRepo struct {
 	lock    sync.RWMutex
-	sources []BinaryFileSource
+	sources []modTimeSource
 	// TODO (jakehehrlich): give 'builds' a more descriptive name
 	builds map[string]*buildInfo
 }
@@ -74,7 +90,16 @@
 	}
 }
 
-func (s *SymbolizerRepo) loadSource(source BinaryFileSource) error {
+func (s *SymbolizerRepo) loadSource(sourcePtr *modTimeSource) error {
+	source := *sourcePtr
+	time, err := source.getModTime()
+	if err != nil {
+		return err
+	}
+	if !time.After(source.modTime) {
+		return nil
+	}
+	sourcePtr.modTime = *time
 	bins, err := source.getBinaries()
 	if err != nil {
 		return err
@@ -94,13 +119,13 @@
 
 // AddSource adds a source of binaries and all contained binaries.
 func (s *SymbolizerRepo) AddSource(source BinaryFileSource) error {
-	s.sources = append(s.sources, source)
-	return s.loadSource(source)
+	s.sources = append(s.sources, modTimeSource{source, time.Unix(0, 0)})
+	return s.loadSource(&s.sources[len(s.sources)-1])
 }
 
 func (s *SymbolizerRepo) reloadSources() error {
 	for _, source := range s.sources {
-		if err := s.loadSource(source); err != nil {
+		if err := s.loadSource(&source); err != nil {
 			return err
 		}
 	}