Skip to content

Commit 7e7ea35

Browse files
author
Luca Bianconi
committed
feat(wip): purge cache after expiration time
1 parent 34762a6 commit 7e7ea35

File tree

10 files changed

+217
-11
lines changed

10 files changed

+217
-11
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ venv
2828
/docsgen/arduino-cli.exe
2929
/docs/rpc/*.md
3030
/docs/commands/*.md
31+
32+
# Delve debugger binary file
33+
__debug_bin

arduino/sketch/sketch.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,5 +302,10 @@ func GenBuildPath(sketchPath *paths.Path) *paths.Path {
302302
}
303303
md5SumBytes := md5.Sum([]byte(path))
304304
md5Sum := strings.ToUpper(hex.EncodeToString(md5SumBytes[:]))
305-
return paths.TempDir().Join("arduino", "sketch-"+md5Sum)
305+
306+
return getSketchesCacheDir().Join("sketch-" + md5Sum)
307+
}
308+
309+
func getSketchesCacheDir() *paths.Path {
310+
return paths.TempDir().Join("arduino", "sketches").Canonical()
306311
}

buildcache/build_cache.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2020 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package buildcache
17+
18+
import (
19+
"os"
20+
"time"
21+
22+
"github.com/arduino/go-paths-helper"
23+
"github.com/pkg/errors"
24+
"github.com/sirupsen/logrus"
25+
)
26+
27+
const (
28+
holdFileName = ".cache-metadata.yaml"
29+
)
30+
31+
func getHoldFileModTime(p string) time.Time {
32+
fileInfo, err := paths.New(p).Stat()
33+
if err != nil {
34+
return time.Unix(0, 0)
35+
}
36+
return fileInfo.ModTime()
37+
}
38+
39+
func updateHoldFile(p string) error {
40+
return paths.New(p).WriteFile([]byte{})
41+
}
42+
43+
// UpdateLastUsedTime registers the last used time in the cache metadata file
44+
func UpdateLastUsedTime(path *paths.Path) error {
45+
dir := requireDirectory(path)
46+
return updateHoldFile(dir.Join(holdFileName).String())
47+
}
48+
49+
// Purge removes all cache directories within baseDir that have expired
50+
// To know how long ago a directory has been last used
51+
// it checks into the hold file. If the file does not exist
52+
// then the directory is purged.
53+
func Purge(baseDir *paths.Path, ttl time.Duration) {
54+
files, err := os.ReadDir(baseDir.String())
55+
if err != nil {
56+
return
57+
}
58+
for _, file := range files {
59+
if file.IsDir() {
60+
deleteIfExpired(baseDir.Join(file.Name()), ttl)
61+
}
62+
}
63+
}
64+
65+
func deleteIfExpired(dir *paths.Path, ttl time.Duration) {
66+
modTime := getHoldFileModTime(dir.Join(holdFileName).String())
67+
if time.Since(modTime) < ttl {
68+
return
69+
}
70+
logrus.Tracef(`Pruning cache directory "%s". Expired by %s`, dir, (time.Since(modTime) - ttl))
71+
err := os.RemoveAll(dir.String())
72+
73+
if err != nil {
74+
logrus.Tracef(`Error while pruning cache directory "%s".\n%s\n`, dir, errors.WithStack(err))
75+
}
76+
}
77+
78+
// requireDirectory returns p if it's a directory, its parent directory otherwise
79+
func requireDirectory(p *paths.Path) *paths.Path {
80+
directoryPath := p
81+
if !directoryPath.IsDir() {
82+
directoryPath = p.Parent()
83+
}
84+
return directoryPath
85+
}

buildcache/build_cache_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package buildcache
2+
3+
import (
4+
"os"
5+
"testing"
6+
"time"
7+
8+
"github.com/arduino/go-paths-helper"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func Test_UpdateLastUsedFileNotExisting(t *testing.T) {
13+
14+
testBuildDir := paths.New(t.TempDir(), "sketches", "sketch-xxx")
15+
err := os.MkdirAll(testBuildDir.String(), 0770)
16+
require.Nil(t, err)
17+
requireCorrectUpdate(t, testBuildDir)
18+
}
19+
20+
func Test_UpdateLastUsedFileExisting(t *testing.T) {
21+
22+
testBuildDir := paths.New(t.TempDir(), "sketches", "sketch-xxx")
23+
err := os.MkdirAll(testBuildDir.String(), 0770)
24+
require.Nil(t, err)
25+
26+
// create the file
27+
err = paths.New(testBuildDir.Join(holdFileName).String()).WriteFile([]byte{})
28+
require.Nil(t, err)
29+
30+
requireCorrectUpdate(t, testBuildDir)
31+
}
32+
33+
func requireCorrectUpdate(t *testing.T, dir *paths.Path) {
34+
timeBeforeUpdating := time.Now()
35+
err := UpdateLastUsedTime(dir)
36+
require.Nil(t, err)
37+
expectedMetadataFile := dir.Join(holdFileName)
38+
_, err = os.Stat(expectedMetadataFile.String())
39+
require.Nil(t, err)
40+
41+
// content can be decoded
42+
err = updateHoldFile(expectedMetadataFile.String())
43+
require.Nil(t, err)
44+
lastUsedTime := getHoldFileModTime(expectedMetadataFile.String())
45+
require.GreaterOrEqual(t, lastUsedTime, timeBeforeUpdating)
46+
}
47+
48+
func TestPurge(t *testing.T) {
49+
ttl := time.Minute
50+
51+
dirToPurge := paths.New(t.TempDir(), "root")
52+
53+
lastUsedTimesByDirPath := map[*paths.Path]time.Time{
54+
(dirToPurge.Join("old")): time.Now().Add(-ttl - time.Hour),
55+
(dirToPurge.Join("fresh")): time.Now().Add(-ttl + time.Minute),
56+
}
57+
58+
// create the metadata files
59+
for dirPath, lastUsedTime := range lastUsedTimesByDirPath {
60+
err := os.MkdirAll(dirPath.Canonical().String(), 0770)
61+
require.Nil(t, err)
62+
holdFilePath := dirPath.Join(holdFileName).Canonical().String()
63+
err = updateHoldFile(holdFilePath)
64+
require.Nil(t, err)
65+
// make sure access time does not matter
66+
accesstime := time.Now()
67+
err = os.Chtimes(holdFilePath, accesstime, lastUsedTime)
68+
require.Nil(t, err)
69+
}
70+
71+
Purge(dirToPurge, ttl)
72+
73+
fileinfo, err := os.Stat(dirToPurge.Join("fresh").String())
74+
require.Nil(t, err)
75+
require.True(t, fileinfo.IsDir())
76+
_, err = os.Stat(dirToPurge.Join("old").String())
77+
require.ErrorIs(t, err, os.ErrNotExist)
78+
}

commands/compile/compile.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import (
2626
"github.com/arduino/arduino-cli/arduino/cores"
2727
"github.com/arduino/arduino-cli/arduino/cores/packagemanager"
2828
"github.com/arduino/arduino-cli/arduino/sketch"
29+
"github.com/arduino/arduino-cli/buildcache"
2930
"github.com/arduino/arduino-cli/commands"
3031
"github.com/arduino/arduino-cli/configuration"
3132
"github.com/arduino/arduino-cli/i18n"
33+
"github.com/arduino/arduino-cli/inventory"
3234
"github.com/arduino/arduino-cli/legacy/builder"
3335
"github.com/arduino/arduino-cli/legacy/builder/types"
3436
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
@@ -64,6 +66,8 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
6466
if lm == nil {
6567
return nil, &arduino.InvalidInstanceError{}
6668
}
69+
// cache is purged after compilation to not remove entries that might be required
70+
defer maybePurgeBuildCache()
6771

6872
logrus.Tracef("Compile %s for %s started", req.GetSketchPath(), req.GetFqbn())
6973
if req.GetSketchPath() == "" {
@@ -142,8 +146,7 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
142146

143147
// Optimize for debug
144148
builderCtx.OptimizeForDebug = req.GetOptimizeForDebug()
145-
146-
builderCtx.CoreBuildCachePath = paths.TempDir().Join("arduino", "core-cache")
149+
builderCtx.CoreBuildCachePath = paths.TempDir().Join("arduino", "cores")
147150

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

@@ -213,6 +216,8 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
213216
r.UsedLibraries = importedLibs
214217
}()
215218

219+
defer buildcache.UpdateLastUsedTime(builderCtx.BuildPath)
220+
216221
// if it's a regular build, go on...
217222
if err := builder.RunBuilder(builderCtx); err != nil {
218223
return r, &arduino.CompileFailedError{Message: err.Error()}
@@ -268,3 +273,24 @@ func Compile(ctx context.Context, req *rpc.CompileRequest, outStream, errStream
268273

269274
return r, nil
270275
}
276+
277+
// maybePurgeBuildCache runs the build files cache purge if the policy conditions are met.
278+
func maybePurgeBuildCache() {
279+
280+
compilationSinceLastPurge := inventory.Store.GetInt("build_cache.compilation_count_since_last_purge")
281+
compilationSinceLastPurge++
282+
inventory.Store.Set("build_cache.compilation_count_since_last_purge", compilationSinceLastPurge)
283+
defer inventory.WriteStore()
284+
285+
// 0 means never purge
286+
purgeAfterCompilationCount := configuration.Settings.GetInt("build_cache.purge_at_compilation_count")
287+
288+
if purgeAfterCompilationCount == 0 || compilationSinceLastPurge < purgeAfterCompilationCount {
289+
return
290+
}
291+
inventory.Store.Set("build_cache.compilation_count_since_last_purge", 0)
292+
293+
cacheTTL := configuration.Settings.GetDuration("build_cache.ttl")
294+
buildcache.Purge(paths.TempDir().Join("arduino", "cores"), cacheTTL)
295+
buildcache.Purge(paths.TempDir().Join("arduino", "sketches"), cacheTTL)
296+
}

configuration/defaults.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package configuration
1818
import (
1919
"path/filepath"
2020
"strings"
21+
"time"
2122

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

4243
// Sketch compilation
4344
settings.SetDefault("sketch.always_export_binaries", false)
45+
settings.SetDefault("build_cache.ttl", time.Hour*24*7)
46+
settings.SetDefault("build_cache.purge_at_compilation_count", 10)
4447

4548
// daemon settings
4649
settings.SetDefault("daemon.port", "50051")

legacy/builder/add_additional_entries_to_context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func (*AddAdditionalEntriesToContext) Run(ctx *types.Context) error {
3535
if err != nil {
3636
return errors.WithStack(err)
3737
}
38-
librariesBuildPath, err := buildPath.Join("libraries").Abs()
38+
librariesBuildPath, err := buildPath.Join(constants.FOLDER_LIBRARIES).Abs()
3939
if err != nil {
4040
return errors.WithStack(err)
4141
}

legacy/builder/constants/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ const FOLDER_CORE = "core"
4747
const FOLDER_PREPROC = "preproc"
4848
const FOLDER_SKETCH = "sketch"
4949
const FOLDER_TOOLS = "tools"
50+
const FOLDER_LIBRARIES = "libraries"
5051
const LIBRARY_ALL_ARCHS = "*"
5152
const LIBRARY_EMAIL = "email"
5253
const LIBRARY_FOLDER_ARCH = "arch"

legacy/builder/phases/core_builder.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"os"
2020
"strings"
2121

22+
"github.com/arduino/arduino-cli/buildcache"
2223
"github.com/arduino/arduino-cli/i18n"
2324
"github.com/arduino/arduino-cli/legacy/builder/builder_utils"
2425
"github.com/arduino/arduino-cli/legacy/builder/constants"
@@ -91,10 +92,12 @@ func compileCore(ctx *types.Context, buildPath *paths.Path, buildCachePath *path
9192
realCoreFolder := coreFolder.Parent().Parent()
9293

9394
var targetArchivedCore *paths.Path
95+
var targetArchivedCoreDir *paths.Path
9496
if buildCachePath != nil {
95-
archivedCoreName := GetCachedCoreArchiveFileName(buildProperties.Get(constants.BUILD_PROPERTIES_FQBN),
97+
archivedCoreName := GetCachedCoreArchiveDirName(buildProperties.Get(constants.BUILD_PROPERTIES_FQBN),
9698
buildProperties.Get("compiler.optimization_flags"), realCoreFolder)
97-
targetArchivedCore = buildCachePath.Join(archivedCoreName)
99+
targetArchivedCoreDir = buildCachePath.Join(archivedCoreName)
100+
targetArchivedCore = targetArchivedCoreDir.Join("core.a")
98101
canUseArchivedCore := !ctx.OnlyUpdateCompilationDatabase &&
99102
!ctx.Clean &&
100103
!builder_utils.CoreOrReferencedCoreHasChanged(realCoreFolder, targetCoreFolder, targetArchivedCore)
@@ -120,6 +123,8 @@ func compileCore(ctx *types.Context, buildPath *paths.Path, buildCachePath *path
120123

121124
// archive core.a
122125
if targetArchivedCore != nil && !ctx.OnlyUpdateCompilationDatabase {
126+
defer buildcache.UpdateLastUsedTime(targetArchivedCore)
127+
targetArchivedCoreDir.MkdirAll()
123128
err := archiveFile.CopyTo(targetArchivedCore)
124129
if ctx.Verbose {
125130
if err == nil {
@@ -137,19 +142,19 @@ func compileCore(ctx *types.Context, buildPath *paths.Path, buildCachePath *path
137142
return archiveFile, variantObjectFiles, nil
138143
}
139144

140-
// GetCachedCoreArchiveFileName returns the filename to be used to store
145+
// GetCachedCoreArchiveDirName returns the directory name to be used to store
141146
// the global cached core.a.
142-
func GetCachedCoreArchiveFileName(fqbn string, optimizationFlags string, coreFolder *paths.Path) string {
147+
func GetCachedCoreArchiveDirName(fqbn string, optimizationFlags string, coreFolder *paths.Path) string {
143148
fqbnToUnderscore := strings.Replace(fqbn, ":", "_", -1)
144149
fqbnToUnderscore = strings.Replace(fqbnToUnderscore, "=", "_", -1)
145150
if absCoreFolder, err := coreFolder.Abs(); err == nil {
146151
coreFolder = absCoreFolder
147152
} // silently continue if absolute path can't be detected
148153
hash := utils.MD5Sum([]byte(coreFolder.String() + optimizationFlags))
149-
realName := "core_" + fqbnToUnderscore + "_" + hash + ".a"
154+
realName := "core_" + fqbnToUnderscore + "_" + hash
150155
if len(realName) > 100 {
151156
// avoid really long names, simply hash the final part
152-
realName = "core_" + utils.MD5Sum([]byte(fqbnToUnderscore+"_"+hash)) + ".a"
157+
realName = "core_" + utils.MD5Sum([]byte(fqbnToUnderscore+"_"+hash))
153158
}
154159
return realName
155160
}

legacy/builder/test/builder_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ func TestBuilderCacheCoreAFile(t *testing.T) {
380380

381381
// Pick timestamp of cached core
382382
coreFolder := paths.New("downloaded_hardware", "arduino", "avr")
383-
coreFileName := phases.GetCachedCoreArchiveFileName(ctx.FQBN.String(), ctx.OptimizationFlags, coreFolder)
383+
coreFileName := phases.GetCachedCoreArchiveDirName(ctx.FQBN.String(), ctx.OptimizationFlags, coreFolder)
384384
cachedCoreFile := ctx.CoreBuildCachePath.Join(coreFileName)
385385
coreStatBefore, err := cachedCoreFile.Stat()
386386
require.NoError(t, err)

0 commit comments

Comments
 (0)