platform_build_soong/finder/finder.go
Lukacs T. Berki e3487c8848 Add a test for correctness of C++ compilation.
This required the following:

- Adding Platform_base_sdk_extension_version to default soong.variables
- Teaching the symlink tree creation code to understand symlinks
- Making finder.go follow symlinks when requested

Adding yet another knob is unfortunate, but I can't allow that
unconditionally because the Android code base contains a number of
symlinks giving rise to infinite directory trees because they point back
to their parent and this seemed preferable to adding complicated logic
like "follow symlink but if only its fully resolved version does not
point under the source tree".

I could be convinced about the latter, though.

Test: Presubmits.
Change-Id: I453f6b7e5334771f5832c700db00f9d24ed1d82f
2022-05-04 09:12:01 +02:00

1565 lines
46 KiB
Go

// Copyright 2017 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package finder
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"android/soong/finder/fs"
)
// This file provides a Finder struct that can quickly search for files satisfying
// certain criteria.
// This Finder gets its speed partially from parallelism and partially from caching.
// If a Stat call returns the same result as last time, then it means Finder
// can skip the ReadDir call for that dir.
// The primary data structure used by the finder is the field Finder.nodes ,
// which is a tree of nodes of type *pathMap .
// Each node represents a directory on disk, along with its stats, subdirectories,
// and contained files.
// The common use case for the Finder is that the caller creates a Finder and gives
// it the same query that was given to it in the previous execution.
// In this situation, the major events that take place are:
// 1. The Finder begins to load its db
// 2. The Finder begins to stat the directories mentioned in its db (using multiple threads)
// Calling Stat on each of these directories is generally a large fraction of the total time
// 3. The Finder begins to construct a separate tree of nodes in each of its threads
// 4. The Finder merges the individual node trees into the main node tree
// 5. The Finder may call ReadDir a few times if there are a few directories that are out-of-date
// These ReadDir calls might prompt additional Stat calls, etc
// 6. The Finder waits for all loading to complete
// 7. The Finder searches the cache for files matching the user's query (using multiple threads)
// These are the invariants regarding concurrency:
// 1. The public methods of Finder are threadsafe.
// The public methods are only performance-optimized for one caller at a time, however.
// For the moment, multiple concurrent callers shouldn't expect any better performance than
// multiple serial callers.
// 2. While building the node tree, only one thread may ever access the <children> collection of a
// *pathMap at once.
// a) The thread that accesses the <children> collection is the thread that discovers the
// children (by reading them from the cache or by having received a response to ReadDir).
// 1) Consequently, the thread that discovers the children also spawns requests to stat
// subdirs.
// b) Consequently, while building the node tree, no thread may do a lookup of its
// *pathMap via filepath because another thread may be adding children to the
// <children> collection of an ancestor node. Additionally, in rare cases, another thread
// may be removing children from an ancestor node if the children were only discovered to
// be irrelevant after calling ReadDir (which happens if a prune-file was just added).
// 3. No query will begin to be serviced until all loading (both reading the db
// and scanning the filesystem) is complete.
// Tests indicate that it only takes about 10% as long to search the in-memory cache as to
// generate it, making this not a huge loss in performance.
// 4. The parsing of the db and the initial setup of the pathMap tree must complete before
// beginning to call listDirSync (because listDirSync can create new entries in the pathMap)
// see cmd/finder.go or finder_test.go for usage examples
// Update versionString whenever making a backwards-incompatible change to the cache file format
const versionString = "Android finder version 1"
// a CacheParams specifies which files and directories the user wishes be scanned and
// potentially added to the cache
type CacheParams struct {
// WorkingDirectory is used as a base for any relative file paths given to the Finder
WorkingDirectory string
// RootDirs are the root directories used to initiate the search
RootDirs []string
// Whether symlinks are followed. If set, symlinks back to their own parent
// directory don't work.
FollowSymlinks bool
// ExcludeDirs are directory names that if encountered are removed from the search
ExcludeDirs []string
// PruneFiles are file names that if encountered prune their entire directory
// (including siblings)
PruneFiles []string
// IncludeFiles are file names to include as matches
IncludeFiles []string
// IncludeSuffixes are filename suffixes to include as matches.
IncludeSuffixes []string
}
// a cacheConfig stores the inputs that determine what should be included in the cache
type cacheConfig struct {
CacheParams
// FilesystemView is a unique identifier telling which parts of which file systems
// are readable by the Finder. In practice its value is essentially username@hostname.
// FilesystemView is set to ensure that a cache file copied to another host or
// found by another user doesn't inadvertently get reused.
FilesystemView string
}
func (p *cacheConfig) Dump() ([]byte, error) {
bytes, err := json.Marshal(p)
return bytes, err
}
// a cacheMetadata stores version information about the cache
type cacheMetadata struct {
// The Version enables the Finder to determine whether it can even parse the file
// If the version changes, the entire cache file must be regenerated
Version string
// The CacheParams enables the Finder to determine whether the parameters match
// If the CacheParams change, the Finder can choose how much of the cache file to reuse
// (although in practice, the Finder will probably choose to ignore the entire file anyway)
Config cacheConfig
}
type Logger interface {
Output(calldepth int, s string) error
}
// the Finder is the main struct that callers will want to use
type Finder struct {
// configuration
DbPath string
numDbLoadingThreads int
numSearchingThreads int
cacheMetadata cacheMetadata
logger Logger
filesystem fs.FileSystem
// temporary state
threadPool *threadPool
mutex sync.Mutex
fsErrs []fsErr
errlock sync.Mutex
shutdownWaitgroup sync.WaitGroup
// non-temporary state
modifiedFlag int32
nodes pathMap
}
var defaultNumThreads = runtime.NumCPU() * 2
// New creates a new Finder for use
func New(cacheParams CacheParams, filesystem fs.FileSystem,
logger Logger, dbPath string) (f *Finder, err error) {
return newImpl(cacheParams, filesystem, logger, dbPath, defaultNumThreads)
}
// newImpl is like New but accepts more params
func newImpl(cacheParams CacheParams, filesystem fs.FileSystem,
logger Logger, dbPath string, numThreads int) (f *Finder, err error) {
numDbLoadingThreads := numThreads
numSearchingThreads := numThreads
metadata := cacheMetadata{
Version: versionString,
Config: cacheConfig{
CacheParams: cacheParams,
FilesystemView: filesystem.ViewId(),
},
}
f = &Finder{
numDbLoadingThreads: numDbLoadingThreads,
numSearchingThreads: numSearchingThreads,
cacheMetadata: metadata,
logger: logger,
filesystem: filesystem,
nodes: *newPathMap("/"),
DbPath: dbPath,
shutdownWaitgroup: sync.WaitGroup{},
}
f.loadFromFilesystem()
// check for any filesystem errors
err = f.getErr()
if err != nil {
return nil, err
}
// confirm that every path mentioned in the CacheConfig exists
for _, path := range cacheParams.RootDirs {
if !filepath.IsAbs(path) {
path = filepath.Join(f.cacheMetadata.Config.WorkingDirectory, path)
}
node := f.nodes.GetNode(filepath.Clean(path), false)
if node == nil || node.ModTime == 0 {
return nil, fmt.Errorf("path %v was specified to be included in the cache but does not exist\n", path)
}
}
return f, nil
}
// FindNamed searches for every cached file
func (f *Finder) FindAll() []string {
return f.FindAt("/")
}
// FindNamed searches for every cached file under <rootDir>
func (f *Finder) FindAt(rootDir string) []string {
filter := func(entries DirEntries) (dirNames []string, fileNames []string) {
return entries.DirNames, entries.FileNames
}
return f.FindMatching(rootDir, filter)
}
// FindNamed searches for every cached file named <fileName>
func (f *Finder) FindNamed(fileName string) []string {
return f.FindNamedAt("/", fileName)
}
// FindNamedAt searches under <rootPath> for every file named <fileName>
// The reason a caller might use FindNamedAt instead of FindNamed is if they want
// to limit their search to a subset of the cache
func (f *Finder) FindNamedAt(rootPath string, fileName string) []string {
filter := func(entries DirEntries) (dirNames []string, fileNames []string) {
matches := []string{}
for _, foundName := range entries.FileNames {
if foundName == fileName {
matches = append(matches, foundName)
}
}
return entries.DirNames, matches
}
return f.FindMatching(rootPath, filter)
}
// FindFirstNamed searches for every file named <fileName>
// Whenever it finds a match, it stops search subdirectories
func (f *Finder) FindFirstNamed(fileName string) []string {
return f.FindFirstNamedAt("/", fileName)
}
// FindFirstNamedAt searches for every file named <fileName>
// Whenever it finds a match, it stops search subdirectories
func (f *Finder) FindFirstNamedAt(rootPath string, fileName string) []string {
filter := func(entries DirEntries) (dirNames []string, fileNames []string) {
matches := []string{}
for _, foundName := range entries.FileNames {
if foundName == fileName {
matches = append(matches, foundName)
}
}
if len(matches) > 0 {
return []string{}, matches
}
return entries.DirNames, matches
}
return f.FindMatching(rootPath, filter)
}
// FindMatching is the most general exported function for searching for files in the cache
// The WalkFunc will be invoked repeatedly and is expected to modify the provided DirEntries
// in place, removing file paths and directories as desired.
// WalkFunc will be invoked potentially many times in parallel, and must be threadsafe.
func (f *Finder) FindMatching(rootPath string, filter WalkFunc) []string {
// set up some parameters
scanStart := time.Now()
var isRel bool
workingDir := f.cacheMetadata.Config.WorkingDirectory
isRel = !filepath.IsAbs(rootPath)
if isRel {
rootPath = filepath.Join(workingDir, rootPath)
}
rootPath = filepath.Clean(rootPath)
// ensure nothing else is using the Finder
f.verbosef("FindMatching waiting for finder to be idle\n")
f.lock()
defer f.unlock()
node := f.nodes.GetNode(rootPath, false)
if node == nil {
f.verbosef("No data for path %v ; apparently not included in cache params: %v\n",
rootPath, f.cacheMetadata.Config.CacheParams)
// path is not found; don't do a search
return []string{}
}
// search for matching files
f.verbosef("Finder finding %v using cache\n", rootPath)
results := f.findInCacheMultithreaded(node, filter, f.numSearchingThreads)
// format and return results
if isRel {
for i := 0; i < len(results); i++ {
results[i] = strings.Replace(results[i], workingDir+"/", "", 1)
}
}
sort.Strings(results)
f.verbosef("Found %v files under %v in %v using cache\n",
len(results), rootPath, time.Since(scanStart))
return results
}
// Shutdown declares that the finder is no longer needed and waits for its cleanup to complete
// Currently, that only entails waiting for the database dump to complete.
func (f *Finder) Shutdown() {
f.WaitForDbDump()
}
// WaitForDbDump returns once the database has been written to f.DbPath.
func (f *Finder) WaitForDbDump() {
f.shutdownWaitgroup.Wait()
}
// End of public api
func (f *Finder) goDumpDb() {
if f.wasModified() {
f.shutdownWaitgroup.Add(1)
go func() {
err := f.dumpDb()
if err != nil {
f.verbosef("%v\n", err)
}
f.shutdownWaitgroup.Done()
}()
} else {
f.verbosef("Skipping dumping unmodified db\n")
}
}
// joinCleanPaths is like filepath.Join but is faster because
// joinCleanPaths doesn't have to support paths ending in "/" or containing ".."
func joinCleanPaths(base string, leaf string) string {
if base == "" {
return leaf
}
if base == "/" {
return base + leaf
}
if leaf == "" {
return base
}
return base + "/" + leaf
}
func (f *Finder) verbosef(format string, args ...interface{}) {
f.logger.Output(2, fmt.Sprintf(format, args...))
}
// loadFromFilesystem populates the in-memory cache based on the contents of the filesystem
func (f *Finder) loadFromFilesystem() {
f.threadPool = newThreadPool(f.numDbLoadingThreads)
err := f.startFromExternalCache()
if err != nil {
f.startWithoutExternalCache()
}
f.goDumpDb()
f.threadPool = nil
}
func (f *Finder) startFind(path string) {
if !filepath.IsAbs(path) {
path = filepath.Join(f.cacheMetadata.Config.WorkingDirectory, path)
}
node := f.nodes.GetNode(path, true)
f.statDirAsync(node)
}
func (f *Finder) lock() {
f.mutex.Lock()
}
func (f *Finder) unlock() {
f.mutex.Unlock()
}
// a statResponse is the relevant portion of the response from the filesystem to a Stat call
type statResponse struct {
ModTime int64
Inode uint64
Device uint64
}
// a pathAndStats stores a path and its stats
type pathAndStats struct {
statResponse
Path string
}
// a dirFullInfo stores all of the relevant information we know about a directory
type dirFullInfo struct {
pathAndStats
FileNames []string
}
// a PersistedDirInfo is the information about a dir that we save to our cache on disk
type PersistedDirInfo struct {
// These field names are short because they are repeated many times in the output json file
P string // path
T int64 // modification time
I uint64 // inode number
F []string // relevant filenames contained
}
// a PersistedDirs is the information that we persist for a group of dirs
type PersistedDirs struct {
// the device on which each directory is stored
Device uint64
// the common root path to which all contained dirs are relative
Root string
// the directories themselves
Dirs []PersistedDirInfo
}
// a CacheEntry is the smallest unit that can be read and parsed from the cache (on disk) at a time
type CacheEntry []PersistedDirs
// a DirEntries lists the files and directories contained directly within a specific directory
type DirEntries struct {
Path string
// elements of DirNames are just the dir names; they don't include any '/' character
DirNames []string
// elements of FileNames are just the file names; they don't include '/' character
FileNames []string
}
// a WalkFunc is the type that is passed into various Find functions for determining which
// directories the caller wishes be walked. The WalkFunc is expected to decide which
// directories to walk and which files to consider as matches to the original query.
type WalkFunc func(DirEntries) (dirs []string, files []string)
// a mapNode stores the relevant stats about a directory to be stored in a pathMap
type mapNode struct {
statResponse
FileNames []string
}
// a pathMap implements the directory tree structure of nodes
type pathMap struct {
mapNode
path string
children map[string]*pathMap
// number of descendent nodes, including self
approximateNumDescendents int
}
func newPathMap(path string) *pathMap {
result := &pathMap{path: path, children: make(map[string]*pathMap, 4),
approximateNumDescendents: 1}
return result
}
// GetNode returns the node at <path>
func (m *pathMap) GetNode(path string, createIfNotFound bool) *pathMap {
if len(path) > 0 && path[0] == '/' {
path = path[1:]
}
node := m
for {
if path == "" {
return node
}
index := strings.Index(path, "/")
var firstComponent string
if index >= 0 {
firstComponent = path[:index]
path = path[index+1:]
} else {
firstComponent = path
path = ""
}
child, found := node.children[firstComponent]
if !found {
if createIfNotFound {
child = node.newChild(firstComponent)
} else {
return nil
}
}
node = child
}
}
func (m *pathMap) newChild(name string) (child *pathMap) {
path := joinCleanPaths(m.path, name)
newChild := newPathMap(path)
m.children[name] = newChild
return m.children[name]
}
func (m *pathMap) UpdateNumDescendents() int {
count := 1
for _, child := range m.children {
count += child.approximateNumDescendents
}
m.approximateNumDescendents = count
return count
}
func (m *pathMap) UpdateNumDescendentsRecursive() {
for _, child := range m.children {
child.UpdateNumDescendentsRecursive()
}
m.UpdateNumDescendents()
}
func (m *pathMap) MergeIn(other *pathMap) {
for key, theirs := range other.children {
ours, found := m.children[key]
if found {
ours.MergeIn(theirs)
} else {
m.children[key] = theirs
}
}
if other.ModTime != 0 {
m.mapNode = other.mapNode
}
m.UpdateNumDescendents()
}
func (m *pathMap) DumpAll() []dirFullInfo {
results := []dirFullInfo{}
m.dumpInto("", &results)
return results
}
func (m *pathMap) dumpInto(path string, results *[]dirFullInfo) {
*results = append(*results,
dirFullInfo{
pathAndStats{statResponse: m.statResponse, Path: path},
m.FileNames},
)
for key, child := range m.children {
childPath := joinCleanPaths(path, key)
if len(childPath) == 0 || childPath[0] != '/' {
childPath = "/" + childPath
}
child.dumpInto(childPath, results)
}
}
// a semaphore can be locked by up to <capacity> callers at once
type semaphore struct {
pool chan bool
}
func newSemaphore(capacity int) *semaphore {
return &semaphore{pool: make(chan bool, capacity)}
}
func (l *semaphore) Lock() {
l.pool <- true
}
func (l *semaphore) Unlock() {
<-l.pool
}
// A threadPool runs goroutines and supports throttling and waiting.
// Without throttling, Go may exhaust the maximum number of various resources, such as
// threads or file descriptors, and crash the program.
type threadPool struct {
receivedRequests sync.WaitGroup
activeRequests semaphore
}
func newThreadPool(maxNumConcurrentThreads int) *threadPool {
return &threadPool{
receivedRequests: sync.WaitGroup{},
activeRequests: *newSemaphore(maxNumConcurrentThreads),
}
}
// Run requests to run the given function in its own goroutine
func (p *threadPool) Run(function func()) {
p.receivedRequests.Add(1)
// If Run() was called from within a goroutine spawned by this threadPool,
// then we may need to return from Run() before having capacity to actually
// run <function>.
//
// It's possible that the body of <function> contains a statement (such as a syscall)
// that will cause Go to pin it to a thread, or will contain a statement that uses
// another resource that is in short supply (such as a file descriptor), so we can't
// actually run <function> until we have capacity.
//
// However, the semaphore used for synchronization is implemented via a channel and
// shouldn't require a new thread for each access.
go func() {
p.activeRequests.Lock()
function()
p.activeRequests.Unlock()
p.receivedRequests.Done()
}()
}
// Wait waits until all goroutines are done, just like sync.WaitGroup's Wait
func (p *threadPool) Wait() {
p.receivedRequests.Wait()
}
type fsErr struct {
path string
err error
}
func (e fsErr) String() string {
return e.path + ": " + e.err.Error()
}
func (f *Finder) serializeCacheEntry(dirInfos []dirFullInfo) ([]byte, error) {
// group each dirFullInfo by its Device, to avoid having to repeat it in the output
dirsByDevice := map[uint64][]PersistedDirInfo{}
for _, entry := range dirInfos {
_, found := dirsByDevice[entry.Device]
if !found {
dirsByDevice[entry.Device] = []PersistedDirInfo{}
}
dirsByDevice[entry.Device] = append(dirsByDevice[entry.Device],
PersistedDirInfo{P: entry.Path, T: entry.ModTime, I: entry.Inode, F: entry.FileNames})
}
cacheEntry := CacheEntry{}
for device, infos := range dirsByDevice {
// find common prefix
prefix := ""
if len(infos) > 0 {
prefix = infos[0].P
}
for _, info := range infos {
for !strings.HasPrefix(info.P+"/", prefix+"/") {
prefix = filepath.Dir(prefix)
if prefix == "/" {
break
}
}
}
// remove common prefix
for i := range infos {
suffix := strings.Replace(infos[i].P, prefix, "", 1)
if len(suffix) > 0 && suffix[0] == '/' {
suffix = suffix[1:]
}
infos[i].P = suffix
}
// turn the map (keyed by device) into a list of structs with labeled fields
// this is to improve readability of the output
cacheEntry = append(cacheEntry, PersistedDirs{Device: device, Root: prefix, Dirs: infos})
}
// convert to json.
// it would save some space to use a different format than json for the db file,
// but the space and time savings are small, and json is easy for humans to read
bytes, err := json.Marshal(cacheEntry)
return bytes, err
}
func (f *Finder) parseCacheEntry(bytes []byte) ([]dirFullInfo, error) {
var cacheEntry CacheEntry
err := json.Unmarshal(bytes, &cacheEntry)
if err != nil {
return nil, err
}
// convert from a CacheEntry to a []dirFullInfo (by copying a few fields)
capacity := 0
for _, element := range cacheEntry {
capacity += len(element.Dirs)
}
nodes := make([]dirFullInfo, capacity)
count := 0
for _, element := range cacheEntry {
for _, dir := range element.Dirs {
path := joinCleanPaths(element.Root, dir.P)
nodes[count] = dirFullInfo{
pathAndStats: pathAndStats{
statResponse: statResponse{
ModTime: dir.T, Inode: dir.I, Device: element.Device,
},
Path: path},
FileNames: dir.F}
count++
}
}
return nodes, nil
}
// We use the following separator byte to distinguish individually parseable blocks of json
// because we know this separator won't appear in the json that we're parsing.
//
// The newline byte can only appear in a UTF-8 stream if the newline character appears, because:
// - The newline character is encoded as "0000 1010" in binary ("0a" in hex)
// - UTF-8 dictates that bytes beginning with a "0" bit are never emitted as part of a multibyte
// character.
//
// We know that the newline character will never appear in our json string, because:
// - If a newline character appears as part of a data string, then json encoding will
// emit two characters instead: '\' and 'n'.
// - The json encoder that we use doesn't emit the optional newlines between any of its
// other outputs.
const lineSeparator = byte('\n')
func (f *Finder) readLine(reader *bufio.Reader) ([]byte, error) {
return reader.ReadBytes(lineSeparator)
}
// validateCacheHeader reads the cache header from cacheReader and tells whether the cache is compatible with this Finder
func (f *Finder) validateCacheHeader(cacheReader *bufio.Reader) bool {
cacheVersionBytes, err := f.readLine(cacheReader)
if err != nil {
f.verbosef("Failed to read database header; database is invalid\n")
return false
}
if len(cacheVersionBytes) > 0 && cacheVersionBytes[len(cacheVersionBytes)-1] == lineSeparator {
cacheVersionBytes = cacheVersionBytes[:len(cacheVersionBytes)-1]
}
cacheVersionString := string(cacheVersionBytes)
currentVersion := f.cacheMetadata.Version
if cacheVersionString != currentVersion {
f.verbosef("Version changed from %q to %q, database is not applicable\n", cacheVersionString, currentVersion)
return false
}
cacheParamBytes, err := f.readLine(cacheReader)
if err != nil {
f.verbosef("Failed to read database search params; database is invalid\n")
return false
}
if len(cacheParamBytes) > 0 && cacheParamBytes[len(cacheParamBytes)-1] == lineSeparator {
cacheParamBytes = cacheParamBytes[:len(cacheParamBytes)-1]
}
currentParamBytes, err := f.cacheMetadata.Config.Dump()
if err != nil {
panic("Finder failed to serialize its parameters")
}
cacheParamString := string(cacheParamBytes)
currentParamString := string(currentParamBytes)
if cacheParamString != currentParamString {
f.verbosef("Params changed from %q to %q, database is not applicable\n", cacheParamString, currentParamString)
return false
}
return true
}
// loadBytes compares the cache info in <data> to the state of the filesystem
// loadBytes returns a map representing <data> and also a slice of dirs that need to be re-walked
func (f *Finder) loadBytes(id int, data []byte) (m *pathMap, dirsToWalk []string, err error) {
helperStartTime := time.Now()
cachedNodes, err := f.parseCacheEntry(data)
if err != nil {
return nil, nil, fmt.Errorf("Failed to parse block %v: %v\n", id, err.Error())
}
unmarshalDate := time.Now()
f.verbosef("Unmarshaled %v objects for %v in %v\n",
len(cachedNodes), id, unmarshalDate.Sub(helperStartTime))
tempMap := newPathMap("/")
stats := make([]statResponse, len(cachedNodes))
for i, node := range cachedNodes {
// check the file system for an updated timestamp
stats[i] = f.statDirSync(node.Path)
}
dirsToWalk = []string{}
for i, cachedNode := range cachedNodes {
updated := stats[i]
// save the cached value
container := tempMap.GetNode(cachedNode.Path, true)
container.mapNode = mapNode{statResponse: updated}
// if the metadata changed and the directory still exists, then
// make a note to walk it later
if !f.isInfoUpToDate(cachedNode.statResponse, updated) && updated.ModTime != 0 {
f.setModified()
// make a note that the directory needs to be walked
dirsToWalk = append(dirsToWalk, cachedNode.Path)
} else {
container.mapNode.FileNames = cachedNode.FileNames
}
}
// count the number of nodes to improve our understanding of the shape of the tree,
// thereby improving parallelism of subsequent searches
tempMap.UpdateNumDescendentsRecursive()
f.verbosef("Statted inodes of block %v in %v\n", id, time.Now().Sub(unmarshalDate))
return tempMap, dirsToWalk, nil
}
// startFromExternalCache loads the cache database from disk
// startFromExternalCache waits to return until the load of the cache db is complete, but
// startFromExternalCache does not wait for all every listDir() or statDir() request to complete
func (f *Finder) startFromExternalCache() (err error) {
startTime := time.Now()
dbPath := f.DbPath
// open cache file and validate its header
reader, err := f.filesystem.Open(dbPath)
if err != nil {
return errors.New("No data to load from database\n")
}
defer reader.Close()
bufferedReader := bufio.NewReader(reader)
if !f.validateCacheHeader(bufferedReader) {
return errors.New("Cache header does not match")
}
f.verbosef("Database header matches, will attempt to use database %v\n", f.DbPath)
// read the file and spawn threads to process it
nodesToWalk := [][]*pathMap{}
mainTree := newPathMap("/")
// read the blocks and stream them into <blockChannel>
type dataBlock struct {
id int
err error
data []byte
}
blockChannel := make(chan dataBlock, f.numDbLoadingThreads)
readBlocks := func() {
index := 0
for {
// It takes some time to unmarshal the input from json, so we want
// to unmarshal it in parallel. In order to find valid places to
// break the input, we scan for the line separators that we inserted
// (for this purpose) when we dumped the database.
data, err := f.readLine(bufferedReader)
var response dataBlock
done := false
if err != nil && err != io.EOF {
response = dataBlock{id: index, err: err, data: nil}
done = true
} else {
done = (err == io.EOF)
response = dataBlock{id: index, err: nil, data: data}
}
blockChannel <- response
index++
duration := time.Since(startTime)
f.verbosef("Read block %v after %v\n", index, duration)
if done {
f.verbosef("Read %v blocks in %v\n", index, duration)
close(blockChannel)
return
}
}
}
go readBlocks()
// Read from <blockChannel> and stream the responses into <resultChannel>.
type workResponse struct {
id int
err error
tree *pathMap
updatedDirs []string
}
resultChannel := make(chan workResponse)
processBlocks := func() {
numProcessed := 0
threadPool := newThreadPool(f.numDbLoadingThreads)
for {
// get a block to process
block, received := <-blockChannel
if !received {
break
}
if block.err != nil {
resultChannel <- workResponse{err: block.err}
break
}
numProcessed++
// wait until there is CPU available to process it
threadPool.Run(
func() {
processStartTime := time.Now()
f.verbosef("Starting to process block %v after %v\n",
block.id, processStartTime.Sub(startTime))
tempMap, updatedDirs, err := f.loadBytes(block.id, block.data)
var response workResponse
if err != nil {
f.verbosef(
"Block %v failed to parse with error %v\n",
block.id, err)
response = workResponse{err: err}
} else {
response = workResponse{
id: block.id,
err: nil,
tree: tempMap,
updatedDirs: updatedDirs,
}
}
f.verbosef("Processed block %v in %v\n",
block.id, time.Since(processStartTime),
)
resultChannel <- response
},
)
}
threadPool.Wait()
f.verbosef("Finished processing %v blocks in %v\n",
numProcessed, time.Since(startTime))
close(resultChannel)
}
go processBlocks()
// Read from <resultChannel> and use the results
combineResults := func() (err error) {
for {
result, received := <-resultChannel
if !received {
break
}
if err != nil {
// In case of an error, wait for work to complete before
// returning the error. This ensures that any subsequent
// work doesn't need to compete for resources (and possibly
// fail due to, for example, a filesystem limit on the number of
// concurrently open files) with past work.
continue
}
if result.err != nil {
err = result.err
continue
}
// update main tree
mainTree.MergeIn(result.tree)
// record any new directories that we will need to Stat()
updatedNodes := make([]*pathMap, len(result.updatedDirs))
for j, dir := range result.updatedDirs {
node := mainTree.GetNode(dir, false)
updatedNodes[j] = node
}
nodesToWalk = append(nodesToWalk, updatedNodes)
}
return err
}
err = combineResults()
if err != nil {
return err
}
f.nodes = *mainTree
// after having loaded the entire db and therefore created entries for
// the directories we know of, now it's safe to start calling ReadDir on
// any updated directories
for i := range nodesToWalk {
f.listDirsAsync(nodesToWalk[i])
}
f.verbosef("Loaded db and statted known dirs in %v\n", time.Since(startTime))
f.threadPool.Wait()
f.verbosef("Loaded db and statted all dirs in %v\n", time.Now().Sub(startTime))
return err
}
// startWithoutExternalCache starts scanning the filesystem according to the cache config
// startWithoutExternalCache should be called if startFromExternalCache is not applicable
func (f *Finder) startWithoutExternalCache() {
startTime := time.Now()
configDirs := f.cacheMetadata.Config.RootDirs
// clean paths
candidates := make([]string, len(configDirs))
for i, dir := range configDirs {
candidates[i] = filepath.Clean(dir)
}
// remove duplicates
dirsToScan := make([]string, 0, len(configDirs))
for _, candidate := range candidates {
include := true
for _, included := range dirsToScan {
if included == "/" || strings.HasPrefix(candidate+"/", included+"/") {
include = false
break
}
}
if include {
dirsToScan = append(dirsToScan, candidate)
}
}
// start searching finally
for _, path := range dirsToScan {
f.verbosef("Starting find of %v\n", path)
f.startFind(path)
}
f.threadPool.Wait()
f.verbosef("Scanned filesystem (not using cache) in %v\n", time.Now().Sub(startTime))
}
// isInfoUpToDate tells whether <new> can confirm that results computed at <old> are still valid
func (f *Finder) isInfoUpToDate(old statResponse, new statResponse) (equal bool) {
if old.Inode != new.Inode {
return false
}
if old.ModTime != new.ModTime {
return false
}
if old.Device != new.Device {
return false
}
return true
}
func (f *Finder) wasModified() bool {
return atomic.LoadInt32(&f.modifiedFlag) > 0
}
func (f *Finder) setModified() {
var newVal int32
newVal = 1
atomic.StoreInt32(&f.modifiedFlag, newVal)
}
// sortedDirEntries exports directory entries to facilitate dumping them to the external cache
func (f *Finder) sortedDirEntries() []dirFullInfo {
startTime := time.Now()
nodes := make([]dirFullInfo, 0)
for _, node := range f.nodes.DumpAll() {
if node.ModTime != 0 {
nodes = append(nodes, node)
}
}
discoveryDate := time.Now()
f.verbosef("Generated %v cache entries in %v\n", len(nodes), discoveryDate.Sub(startTime))
less := func(i int, j int) bool {
return nodes[i].Path < nodes[j].Path
}
sort.Slice(nodes, less)
sortDate := time.Now()
f.verbosef("Sorted %v cache entries in %v\n", len(nodes), sortDate.Sub(discoveryDate))
return nodes
}
// serializeDb converts the cache database into a form to save to disk
func (f *Finder) serializeDb() ([]byte, error) {
// sort dir entries
var entryList = f.sortedDirEntries()
// Generate an output file that can be conveniently loaded using the same number of threads
// as were used in this execution (because presumably that will be the number of threads
// used in the next execution too)
// generate header
header := []byte{}
header = append(header, []byte(f.cacheMetadata.Version)...)
header = append(header, lineSeparator)
configDump, err := f.cacheMetadata.Config.Dump()
if err != nil {
return nil, err
}
header = append(header, configDump...)
// serialize individual blocks in parallel
numBlocks := f.numDbLoadingThreads
if numBlocks > len(entryList) {
numBlocks = len(entryList)
}
blocks := make([][]byte, 1+numBlocks)
blocks[0] = header
blockMin := 0
wg := sync.WaitGroup{}
var errLock sync.Mutex
for i := 1; i <= numBlocks; i++ {
// identify next block
blockMax := len(entryList) * i / numBlocks
block := entryList[blockMin:blockMax]
// process block
wg.Add(1)
go func(index int, block []dirFullInfo) {
byteBlock, subErr := f.serializeCacheEntry(block)
f.verbosef("Serialized block %v into %v bytes\n", index, len(byteBlock))
if subErr != nil {
f.verbosef("%v\n", subErr.Error())
errLock.Lock()
err = subErr
errLock.Unlock()
} else {
blocks[index] = byteBlock
}
wg.Done()
}(i, block)
blockMin = blockMax
}
wg.Wait()
if err != nil {
return nil, err
}
content := bytes.Join(blocks, []byte{lineSeparator})
return content, nil
}
// dumpDb saves the cache database to disk
func (f *Finder) dumpDb() error {
startTime := time.Now()
f.verbosef("Dumping db\n")
tempPath := f.DbPath + ".tmp"
bytes, err := f.serializeDb()
if err != nil {
return err
}
serializeDate := time.Now()
f.verbosef("Serialized db in %v\n", serializeDate.Sub(startTime))
// dump file and atomically move
err = f.filesystem.WriteFile(tempPath, bytes, 0777)
if err != nil {
return err
}
err = f.filesystem.Rename(tempPath, f.DbPath)
if err != nil {
return err
}
f.verbosef("Wrote db in %v\n", time.Now().Sub(serializeDate))
return nil
}
// canIgnoreFsErr checks for certain classes of filesystem errors that are safe to ignore
func (f *Finder) canIgnoreFsErr(err error) bool {
pathErr, isPathErr := err.(*os.PathError)
if !isPathErr {
// Don't recognize this error
return false
}
if os.IsPermission(pathErr) {
// Permission errors are ignored:
// https://issuetracker.google.com/37553659
// https://github.com/google/kati/pull/116
return true
}
if pathErr.Err == os.ErrNotExist {
// If a directory doesn't exist, that generally means the cache is out-of-date
return true
}
// Don't recognize this error
return false
}
// onFsError should be called whenever a potentially fatal error is returned from a filesystem call
func (f *Finder) onFsError(path string, err error) {
if !f.canIgnoreFsErr(err) {
// We could send the errors through a channel instead, although that would cause this call
// to block unless we preallocated a sufficient buffer or spawned a reader thread.
// Although it wouldn't be too complicated to spawn a reader thread, it's still slightly
// more convenient to use a lock. Only in an unusual situation should this code be
// invoked anyway.
f.errlock.Lock()
f.fsErrs = append(f.fsErrs, fsErr{path: path, err: err})
f.errlock.Unlock()
}
}
// discardErrsForPrunedPaths removes any errors for paths that are no longer included in the cache
func (f *Finder) discardErrsForPrunedPaths() {
// This function could be somewhat inefficient due to being single-threaded,
// but the length of f.fsErrs should be approximately 0, so it shouldn't take long anyway.
relevantErrs := make([]fsErr, 0, len(f.fsErrs))
for _, fsErr := range f.fsErrs {
path := fsErr.path
node := f.nodes.GetNode(path, false)
if node != nil {
// The path in question wasn't pruned due to a failure to process a parent directory.
// So, the failure to process this path is important
relevantErrs = append(relevantErrs, fsErr)
}
}
f.fsErrs = relevantErrs
}
// getErr returns an error based on previous calls to onFsErr, if any
func (f *Finder) getErr() error {
f.discardErrsForPrunedPaths()
numErrs := len(f.fsErrs)
if numErrs < 1 {
return nil
}
maxNumErrsToInclude := 10
message := ""
if numErrs > maxNumErrsToInclude {
message = fmt.Sprintf("finder encountered %v errors: %v...", numErrs, f.fsErrs[:maxNumErrsToInclude])
} else {
message = fmt.Sprintf("finder encountered %v errors: %v", numErrs, f.fsErrs)
}
return errors.New(message)
}
func (f *Finder) statDirAsync(dir *pathMap) {
node := dir
path := dir.path
f.threadPool.Run(
func() {
updatedStats := f.statDirSync(path)
if !f.isInfoUpToDate(node.statResponse, updatedStats) {
node.mapNode = mapNode{
statResponse: updatedStats,
FileNames: []string{},
}
f.setModified()
if node.statResponse.ModTime != 0 {
// modification time was updated, so re-scan for
// child directories
f.listDirAsync(dir)
}
}
},
)
}
func (f *Finder) statDirSync(path string) statResponse {
fileInfo, err := f.filesystem.Lstat(path)
var stats statResponse
if err != nil {
// possibly record this error
f.onFsError(path, err)
// in case of a failure to stat the directory, treat the directory as missing (modTime = 0)
return stats
}
modTime := fileInfo.ModTime()
stats = statResponse{}
inode, err := f.filesystem.InodeNumber(fileInfo)
if err != nil {
panic(fmt.Sprintf("Could not get inode number of %v: %v\n", path, err.Error()))
}
stats.Inode = inode
device, err := f.filesystem.DeviceNumber(fileInfo)
if err != nil {
panic(fmt.Sprintf("Could not get device number of %v: %v\n", path, err.Error()))
}
stats.Device = device
permissionsChangeTime, err := f.filesystem.PermTime(fileInfo)
if err != nil {
panic(fmt.Sprintf("Could not get permissions modification time (CTime) of %v: %v\n", path, err.Error()))
}
// We're only interested in knowing whether anything about the directory
// has changed since last check, so we use the latest of the two
// modification times (content modification (mtime) and
// permission modification (ctime))
if permissionsChangeTime.After(modTime) {
modTime = permissionsChangeTime
}
stats.ModTime = modTime.UnixNano()
return stats
}
func (f *Finder) shouldIncludeFile(fileName string) bool {
for _, includedName := range f.cacheMetadata.Config.IncludeFiles {
if fileName == includedName {
return true
}
}
for _, includeSuffix := range f.cacheMetadata.Config.IncludeSuffixes {
if strings.HasSuffix(fileName, includeSuffix) {
return true
}
}
return false
}
// pruneCacheCandidates removes the items that we don't want to include in our persistent cache
func (f *Finder) pruneCacheCandidates(items *DirEntries) {
for _, fileName := range items.FileNames {
for _, abortedName := range f.cacheMetadata.Config.PruneFiles {
if fileName == abortedName {
items.FileNames = []string{}
items.DirNames = []string{}
return
}
}
}
// remove any files that aren't the ones we want to include
writeIndex := 0
for _, fileName := range items.FileNames {
if f.shouldIncludeFile(fileName) {
items.FileNames[writeIndex] = fileName
writeIndex++
}
}
// resize
items.FileNames = items.FileNames[:writeIndex]
writeIndex = 0
for _, dirName := range items.DirNames {
items.DirNames[writeIndex] = dirName
// ignore other dirs that are known to not be inputs to the build process
include := true
for _, excludedName := range f.cacheMetadata.Config.ExcludeDirs {
if dirName == excludedName {
// don't include
include = false
break
}
}
if include {
writeIndex++
}
}
// resize
items.DirNames = items.DirNames[:writeIndex]
}
func (f *Finder) listDirsAsync(nodes []*pathMap) {
f.threadPool.Run(
func() {
for i := range nodes {
f.listDirSync(nodes[i])
}
},
)
}
func (f *Finder) listDirAsync(node *pathMap) {
f.threadPool.Run(
func() {
f.listDirSync(node)
},
)
}
func (f *Finder) listDirSync(dir *pathMap) {
path := dir.path
children, err := f.filesystem.ReadDir(path)
if err != nil {
// possibly record this error
f.onFsError(path, err)
// if listing the contents of the directory fails (presumably due to
// permission denied), then treat the directory as empty
children = nil
}
var subdirs []string
var subfiles []string
for _, child := range children {
linkBits := child.Mode() & os.ModeSymlink
isLink := linkBits != 0
if isLink {
childPath := filepath.Join(path, child.Name())
childStat, err := f.filesystem.Stat(childPath)
if err != nil {
// If stat fails this is probably a broken or dangling symlink, treat it as a file.
subfiles = append(subfiles, child.Name())
} else if childStat.IsDir() {
// Skip symlink dirs if not requested otherwise. Android has a number
// of symlinks creating infinite source trees which would otherwise get
// us in an infinite loop.
// TODO(b/197349722): Revisit this once symlink loops are banned in the
// source tree.
if f.cacheMetadata.Config.FollowSymlinks {
subdirs = append(subdirs, child.Name())
}
} else {
// We do have to support symlink files because the link name might be
// different than the target name
// (for example, Android.bp -> build/soong/root.bp)
subfiles = append(subfiles, child.Name())
}
} else if child.IsDir() {
subdirs = append(subdirs, child.Name())
} else {
subfiles = append(subfiles, child.Name())
}
}
parentNode := dir
entry := &DirEntries{Path: path, DirNames: subdirs, FileNames: subfiles}
f.pruneCacheCandidates(entry)
// create a pathMap node for each relevant subdirectory
relevantChildren := map[string]*pathMap{}
for _, subdirName := range entry.DirNames {
childNode, found := parentNode.children[subdirName]
// if we already knew of this directory, then we already have a request pending to Stat it
// if we didn't already know of this directory, then we must Stat it now
if !found {
childNode = parentNode.newChild(subdirName)
f.statDirAsync(childNode)
}
relevantChildren[subdirName] = childNode
}
// Note that in rare cases, it's possible that we're reducing the set of
// children via this statement, if these are all true:
// 1. we previously had a cache that knew about subdirectories of parentNode
// 2. the user created a prune-file (described in pruneCacheCandidates)
// inside <parentNode>, which specifies that the contents of parentNode
// are to be ignored.
// The fact that it's possible to remove children here means that *pathMap structs
// must not be looked up from f.nodes by filepath (and instead must be accessed by
// direct pointer) until after every listDirSync completes
parentNode.FileNames = entry.FileNames
parentNode.children = relevantChildren
}
// listMatches takes a node and a function that specifies which subdirectories and
// files to include, and listMatches returns the matches
func (f *Finder) listMatches(node *pathMap,
filter WalkFunc) (subDirs []*pathMap, filePaths []string) {
entries := DirEntries{
FileNames: node.FileNames,
}
entries.DirNames = make([]string, 0, len(node.children))
for childName := range node.children {
entries.DirNames = append(entries.DirNames, childName)
}
dirNames, fileNames := filter(entries)
subDirs = []*pathMap{}
filePaths = make([]string, 0, len(fileNames))
for _, fileName := range fileNames {
filePaths = append(filePaths, joinCleanPaths(node.path, fileName))
}
subDirs = make([]*pathMap, 0, len(dirNames))
for _, childName := range dirNames {
child, ok := node.children[childName]
if ok {
subDirs = append(subDirs, child)
}
}
return subDirs, filePaths
}
// findInCacheMultithreaded spawns potentially multiple goroutines with which to search the cache.
func (f *Finder) findInCacheMultithreaded(node *pathMap, filter WalkFunc,
approxNumThreads int) []string {
if approxNumThreads < 2 {
// Done spawning threads; process remaining directories
return f.findInCacheSinglethreaded(node, filter)
}
totalWork := 0
for _, child := range node.children {
totalWork += child.approximateNumDescendents
}
childrenResults := make(chan []string, len(node.children))
subDirs, filePaths := f.listMatches(node, filter)
// process child directories
for _, child := range subDirs {
numChildThreads := approxNumThreads * child.approximateNumDescendents / totalWork
childProcessor := func(child *pathMap) {
childResults := f.findInCacheMultithreaded(child, filter, numChildThreads)
childrenResults <- childResults
}
// If we're allowed to use more than 1 thread to process this directory,
// then instead we use 1 thread for each subdirectory.
// It would be strange to spawn threads for only some subdirectories.
go childProcessor(child)
}
// collect results
for i := 0; i < len(subDirs); i++ {
childResults := <-childrenResults
filePaths = append(filePaths, childResults...)
}
close(childrenResults)
return filePaths
}
// findInCacheSinglethreaded synchronously searches the cache for all matching file paths
// note findInCacheSinglethreaded runs 2X to 4X as fast by being iterative rather than recursive
func (f *Finder) findInCacheSinglethreaded(node *pathMap, filter WalkFunc) []string {
if node == nil {
return []string{}
}
nodes := []*pathMap{node}
matches := []string{}
for len(nodes) > 0 {
currentNode := nodes[0]
nodes = nodes[1:]
subDirs, filePaths := f.listMatches(currentNode, filter)
nodes = append(nodes, subDirs...)
matches = append(matches, filePaths...)
}
return matches
}