Skip to content

feat: purge build cache #2033

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: purge cache after expiration time
  • Loading branch information
Luca Bianconi committed Jan 17, 2023
commit 165789cf556995e837d41b9b4151bbd1fcd61e04
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ venv
/docsgen/arduino-cli.exe
/docs/rpc/*.md
/docs/commands/*.md

# Delve debugger binary file
__debug_bin
7 changes: 6 additions & 1 deletion arduino/sketch/sketch.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,5 +302,10 @@ func GenBuildPath(sketchPath *paths.Path) *paths.Path {
}
md5SumBytes := md5.Sum([]byte(path))
md5Sum := strings.ToUpper(hex.EncodeToString(md5SumBytes[:]))
return paths.TempDir().Join("arduino", "sketch-"+md5Sum)

return getSketchesCacheDir().Join(md5Sum)
}

func getSketchesCacheDir() *paths.Path {
return paths.TempDir().Join("arduino", "sketches").Canonical()
}
4 changes: 2 additions & 2 deletions arduino/sketch/sketch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,10 @@ func TestNewSketchFolderSymlink(t *testing.T) {
}

func TestGenBuildPath(t *testing.T) {
want := paths.TempDir().Join("arduino", "sketch-ACBD18DB4CC2F85CEDEF654FCCC4A4D8")
want := paths.TempDir().Join("arduino", "sketches", "ACBD18DB4CC2F85CEDEF654FCCC4A4D8")
assert.True(t, GenBuildPath(paths.New("foo")).EquivalentTo(want))

want = paths.TempDir().Join("arduino", "sketch-D41D8CD98F00B204E9800998ECF8427E")
want = paths.TempDir().Join("arduino", "sketches", "D41D8CD98F00B204E9800998ECF8427E")
assert.True(t, GenBuildPath(nil).EquivalentTo(want))
}

Expand Down
72 changes: 72 additions & 0 deletions buildcache/build_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// This file is part of arduino-cli.
//
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package buildcache

import (
"time"

"github.com/arduino/go-paths-helper"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

const lastUsedFileName = ".last-used"

// GetOrCreate retrieves or creates the cache directory at the given path
// If the cache already exists the lifetime of the cache is extended.
func GetOrCreate(dir *paths.Path) (*paths.Path, error) {
if !dir.Exist() {
if err := dir.MkdirAll(); err != nil {
return nil, err
}
}

if err := dir.Join(lastUsedFileName).WriteFile([]byte{}); err != nil {
return nil, err
}
return dir, nil
}

// Purge removes all cache directories within baseDir that have expired
// To know how long ago a directory has been last used
// it checks into the .last-used file.
func Purge(baseDir *paths.Path, ttl time.Duration) {
files, err := baseDir.ReadDir()
if err != nil {
return
}
for _, file := range files {
if file.IsDir() {
removeIfExpired(file, ttl)
}
}
}

func removeIfExpired(dir *paths.Path, ttl time.Duration) {
fileInfo, err := dir.Join().Stat()
if err != nil {
return
}
lifeExpectancy := ttl - time.Since(fileInfo.ModTime())
if lifeExpectancy > 0 {
return
}
logrus.Tracef(`Purging cache directory "%s". Expired by %s`, dir, lifeExpectancy.Abs())
err = dir.RemoveAll()
if err != nil {
logrus.Tracef(`Error while pruning cache directory "%s": %s`, dir, errors.WithStack(err))
}
}
80 changes: 80 additions & 0 deletions buildcache/build_cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// This file is part of arduino-cli.
//
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package buildcache

import (
"testing"
"time"

"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
)

func Test_UpdateLastUsedFileNotExisting(t *testing.T) {
testBuildDir := paths.New(t.TempDir(), "sketches", "xxx")
require.NoError(t, testBuildDir.MkdirAll())
timeBeforeUpdating := time.Unix(0, 0)
requireCorrectUpdate(t, testBuildDir, timeBeforeUpdating)
}

func Test_UpdateLastUsedFileExisting(t *testing.T) {
testBuildDir := paths.New(t.TempDir(), "sketches", "xxx")
require.NoError(t, testBuildDir.MkdirAll())

// create the file
preExistingFile := testBuildDir.Join(lastUsedFileName)
require.NoError(t, preExistingFile.WriteFile([]byte{}))
timeBeforeUpdating := time.Now().Add(-time.Second)
preExistingFile.Chtimes(time.Now(), timeBeforeUpdating)
requireCorrectUpdate(t, testBuildDir, timeBeforeUpdating)
}

func requireCorrectUpdate(t *testing.T, dir *paths.Path, prevModTime time.Time) {
_, err := GetOrCreate(dir)
require.NoError(t, err)
expectedFile := dir.Join(lastUsedFileName)
fileInfo, err := expectedFile.Stat()
require.Nil(t, err)
require.Greater(t, fileInfo.ModTime(), prevModTime)
}

func TestPurge(t *testing.T) {
ttl := time.Minute

dirToPurge := paths.New(t.TempDir(), "root")

lastUsedTimesByDirPath := map[*paths.Path]time.Time{
(dirToPurge.Join("old")): time.Now().Add(-ttl - time.Hour),
(dirToPurge.Join("fresh")): time.Now().Add(-ttl + time.Minute),
}

// create the metadata files
for dirPath, lastUsedTime := range lastUsedTimesByDirPath {
require.NoError(t, dirPath.MkdirAll())
infoFilePath := dirPath.Join(lastUsedFileName).Canonical()
require.NoError(t, infoFilePath.WriteFile([]byte{}))
// make sure access time does not matter
accesstime := time.Now()
require.NoError(t, infoFilePath.Chtimes(accesstime, lastUsedTime))
}

Purge(dirToPurge, ttl)

files, err := dirToPurge.Join("fresh").Stat()
require.Nil(t, err)
require.True(t, files.IsDir())
require.True(t, dirToPurge.Exist())
}
31 changes: 29 additions & 2 deletions commands/compile/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ import (
"github.com/arduino/arduino-cli/arduino/cores"
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
"github.com/arduino/arduino-cli/arduino/sketch"
"github.com/arduino/arduino-cli/buildcache"
"github.com/arduino/arduino-cli/commands"
"github.com/arduino/arduino-cli/configuration"
"github.com/arduino/arduino-cli/i18n"
"github.com/arduino/arduino-cli/inventory"
"github.com/arduino/arduino-cli/legacy/builder"
"github.com/arduino/arduino-cli/legacy/builder/types"
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
Expand Down Expand Up @@ -135,6 +137,11 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
if err = builderCtx.BuildPath.MkdirAll(); err != nil {
return nil, &arduino.PermissionDeniedError{Message: tr("Cannot create build directory"), Cause: err}
}

buildcache.GetOrCreate(builderCtx.BuildPath)
// cache is purged after compilation to not remove entries that might be required
defer maybePurgeBuildCache()

builderCtx.CompilationDatabase = bldr.NewCompilationDatabase(
builderCtx.BuildPath.Join("compile_commands.json"),
)
Expand All @@ -143,8 +150,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream

// Optimize for debug
builderCtx.OptimizeForDebug = req.GetOptimizeForDebug()

builderCtx.CoreBuildCachePath = paths.TempDir().Join("arduino", "core-cache")
builderCtx.CoreBuildCachePath = paths.TempDir().Join("arduino", "cores")

builderCtx.Jobs = int(req.GetJobs())

Expand Down Expand Up @@ -284,3 +290,24 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream

return r, nil
}

// maybePurgeBuildCache runs the build files cache purge if the policy conditions are met.
func maybePurgeBuildCache() {

compilationsBeforePurge := configuration.Settings.GetUint("build_cache.compilations_before_purge")
// 0 means never purge
if compilationsBeforePurge == 0 {
return
}
compilationSinceLastPurge := inventory.Store.GetUint("build_cache.compilation_count_since_last_purge")
compilationSinceLastPurge++
inventory.Store.Set("build_cache.compilation_count_since_last_purge", compilationSinceLastPurge)
defer inventory.WriteStore()
if compilationsBeforePurge == 0 || compilationSinceLastPurge < compilationsBeforePurge {
return
}
inventory.Store.Set("build_cache.compilation_count_since_last_purge", 0)
cacheTTL := configuration.Settings.GetDuration("build_cache.ttl").Abs()
buildcache.Purge(paths.TempDir().Join("arduino", "cores"), cacheTTL)
buildcache.Purge(paths.TempDir().Join("arduino", "sketches"), cacheTTL)
}
3 changes: 3 additions & 0 deletions configuration/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package configuration
import (
"path/filepath"
"strings"
"time"

"github.com/spf13/viper"
)
Expand All @@ -41,6 +42,8 @@ func SetDefaults(settings *viper.Viper) {

// Sketch compilation
settings.SetDefault("sketch.always_export_binaries", false)
settings.SetDefault("build_cache.ttl", time.Hour*24*30)
settings.SetDefault("build_cache.compilations_before_purge", 10)

// daemon settings
settings.SetDefault("daemon.port", "50051")
Expand Down
6 changes: 6 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
to the sketch folder. This is the equivalent of using the [`--export-binaries`][arduino-cli compile options] flag.
- `updater` - configuration options related to Arduino CLI updates
- `enable_notification` - set to `false` to disable notifications of new Arduino CLI releases, defaults to `true`
- `build_cache` configuration options related to the compilation cache
- `compilations_before_purge` - interval, in number of compilations, at which the cache is purged, defaults to `10`.
When `0` the cache is never purged.
- `ttl` - cache expiration time of build folders. If the cache is hit by a compilation the corresponding build files
lifetime is renewed. The value format must be a valid input for
[time.ParseDuration()](https://pkg.go.dev/time#ParseDuration), defaults to `720h` (30 days).

## Configuration methods

Expand Down
1 change: 1 addition & 0 deletions internal/integrationtest/arduino-cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ func NewArduinoCliWithinEnvironment(env *Environment, config *ArduinoCLIConfig)
"ARDUINO_DATA_DIR": cli.dataDir.String(),
"ARDUINO_DOWNLOADS_DIR": cli.stagingDir.String(),
"ARDUINO_SKETCHBOOK_DIR": cli.sketchbookDir.String(),
"ARDUINO_BUILD_CACHE_COMPILATIONS_BEFORE_PURGE": "0",
}
env.RegisterCleanUpCallback(cli.CleanUp)
return cli
Expand Down
Loading