a88c883e3e
ioutil.ReadDir returns []os.FileInfo, which contains information on each entry in the directory that is only available by calling os.Lstat on the entry. Finder only the name and type (regular, directory or symlink) of the files, which on Linux kernels >= 2.6.4 is available in the return values of syscall.Getdents. In preparation for using syscall.Getdents, switch filesystem.ReadDir to return an interface that only contains the information that will be available from syscall.Getdents. Bug: 70897635 Test: m checkbuild Change-Id: Id2749d709a0f7b5a61abedde68549d4bf208a568
1533 lines
45 KiB
Go
1533 lines
45 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
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
}
|
|
|
|
func (f *Finder) waitForDbDump() {
|
|
f.shutdownWaitgroup.Wait()
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
// include only these files
|
|
for _, includedName := range f.cacheMetadata.Config.IncludeFiles {
|
|
if fileName == includedName {
|
|
items.FileNames[writeIndex] = fileName
|
|
writeIndex++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// 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 child.IsDir() {
|
|
if !isLink {
|
|
// Skip symlink dirs.
|
|
// We don't have to support symlink dirs because
|
|
// that would cause duplicates.
|
|
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())
|
|
}
|
|
|
|
}
|
|
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
|
|
}
|