-
Notifications
You must be signed in to change notification settings - Fork 76
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
add caching to git describe #684
Conversation
Generate changelog in
|
2a9b159
to
621fef4
Compare
ab3dc73
to
f8f5576
Compare
src/test/groovy/com/palantir/gradle/gitversion/GitVersionPluginTests.groovy
Outdated
Show resolved
Hide resolved
c4b8dcc
to
2ab9172
Compare
.getSharedServices() | ||
.registerIfAbsent("GitVersionCacheService", GitVersionCacheService.class, spec -> { | ||
// Provide some parameters | ||
spec.getMaxParallelUsages().set(maxParallelUsage); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to set a max parallel usages at all? I think provided we make the class thread safe, it should be ok to allow max parallel access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed this line.
|
||
final Git git = gitRepo(project); | ||
if (project.getRootProject() == project) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it'd be best to leave this in the root plugin, if possible. Just to separate out the code a bit and avoid this if.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added back the GitVersionRootPlugin
. The only reason why I deleted the root plugin is that we'd have to have the following in both plugins
Provider<GitVersionCacheService> serviceProvider = project.getGradle()
.getSharedServices()
.registerIfAbsent("GitVersionCacheService", GitVersionCacheService.class, _spec -> {});
Git git = gitRepo(gitDir); | ||
VersionDetails versionDetails = | ||
TimingVersionDetails.wrap(timer, new VersionDetailsImpl(git, GitVersionArgs.fromGroovyClosure(args))); | ||
versionDetailsMap.put(key, versionDetails); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not thread safe. I'd make the map a ConcurrentHashMap
and use .computeIfAbsent
:
versionDetailsMap.computeIfAbsent(key, _ignored -> {
// calculate version details for key
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have guava as a dependency here - perhaps just a LoadingCache
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A cache that expires is unnecessary complexity for this use case imo, the number of items in this map should never grow too large, and evicting entries is probably not what we want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah well, the checkstyle config bans guava caches in favor of Caffeine anyway :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I resolved by using the concurrent hash map instead of hash map here.
public final String getGitVersion(String project, Object args) { | ||
File gitDir = getRootGitDir(new File(project)); | ||
GitVersionArgs gitVersionArgs = GitVersionArgs.fromGroovyClosure(args); | ||
String key = gitDir.toString() + "|" + gitVersionArgs.getPrefix(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does git dir need to be part of the key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I am thinking of the scenario where different projects have different work tree. I can remove this if it is considered an extreme edge case not likely to happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^^ I removed the gitDir
part in the key. If you think the above case might happen and it is necessary to keep the worktree as part of the key, please let me know.
src/main/java/com/palantir/gradle/gitversion/GitVersionCacheService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/palantir/gradle/gitversion/GitVersionCacheService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/palantir/gradle/gitversion/GitVersionCacheService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/palantir/gradle/gitversion/GitVersionCacheService.java
Outdated
Show resolved
Hide resolved
changelog/@unreleased/pr-684.v2.yml
Outdated
@@ -0,0 +1,5 @@ | |||
type: improvement | |||
improvement: | |||
description: add caching to git describe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a better description here. I'd make it a break
and say it removes support for Gradle 5 (and say what is the new minimum version we support). I'd also a bit more detail, saying how the pattern of calling gitversion()
in each project used to lead to extremely long configuration times, however these are now cached so it's faster.
if (versionDetailsMap.containsKey(key)) { | ||
return versionDetailsMap.get(key).getVersion(); | ||
} | ||
Git git = gitRepo(gitDir); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this could just be pushed down into VersionDetailsImpl
? For a couple of reasons:
- It seems odd to initialize a JGit object in this class just to inject it into the VersionDetailsImpl instance
- If we ever do decide to eliminate JGit, it would be good to have the usage limited in scope to make refactoring easier
On the other hand, I wonder if the Git
class is thread-safe? The JGit javadoc doesn't really specify and a quick search indicates... maybe? If it is, then I wonder if it would make sense to cache a single instance here in this class instead of creating one instance per VersionDetailsImpl.
My preference would probably be to move this inside VersionDetailsImpl
and instead pass gitDir
as a constructor arg; a quick glance at VersionDetailsImpl
seems to indicate that it is using both JGit and Native Git to compute the description, which seems a little broken (see e.g.: https://github.com/palantir/gradle-git-version/blob/develop/src/main/java/com/palantir/gradle/gitversion/VersionDetailsImpl.java#L54-L60)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by passing gitDir
to the constructor rather than Git
object. @bjlaub Do you want me to delete the Native git part as well?
8d686b5
to
0aa0914
Compare
6923741
to
a56c821
Compare
final Git git = gitRepo(project); | ||
Provider<GitVersionCacheService> serviceProvider = project.getGradle() | ||
.getSharedServices() | ||
.registerIfAbsent("GitVersionCacheService", GitVersionCacheService.class, _spec -> {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make a static method to get the Provider<GitVersionCacheService>
from a Project
which is shared by both plugins.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could even put it on GitVersionCacheService
private static final Logger log = LoggerFactory.getLogger(GitVersionCacheService.class); | ||
|
||
private final Timer timer = new Timer(); | ||
private final Map<GitVersionArgs, VersionDetails> versionDetailsMap = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make this a ConcurrentMap
to make it obvious it's the concurrent version.
changelog/@unreleased/pr-684.v2.yml
Outdated
called under allprojects. \n\n## After this PR\nBuild service is used so if we | ||
call `gitVersion()` on all projects, we will not call the expensive `git describe` | ||
many times. Instead, the build service will have a concurrent hash map that stores | ||
the created `VersionDetails` object with a specific prefix as key the first time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this sentence is too much detail for an end user - I'd leave out any details of how the caching happens (build service, hashmaps, prefixes etc) and just say it's now cached.
changelog/@unreleased/pr-684.v2.yml
Outdated
@@ -0,0 +1,16 @@ | |||
type: break | |||
break: | |||
description: "## Before this PR\n`gitVersion()` or `versionDetails()` does not have |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also leave out the "Before this PR" bits and just have a couple of bullet points, one for the change and one describing the break.
8bb771f
to
ce24731
Compare
@@ -1,126 +0,0 @@ | |||
/* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
out of curiosity, what's the reason for removing this? If there's a potential future where we remove jgit, don't we want to keep this code around?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah makes sense. I deleted because it won't be used if we do not call Native git.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because it won't be used if we do not call Native git.
not sure i follow - don't we always try native git first, and then fall back to jgit if it fails?
71d0fe6
to
7b734e5
Compare
private static final Logger log = LoggerFactory.getLogger(GitVersionCacheService.class); | ||
|
||
private final Timer timer = new Timer(); | ||
private final ConcurrentMap<GitVersionArgs, VersionDetails> versionDetailsMap = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried this out on a project and was wondering why it doesn't appear to produce a speedup on repos with large numbers of projects and calling gitversion()
on each. I think this is the reason - GitVersionArgs
does not implement hashCode()
or equals()
so a new entry is created on the map each time. I'd either just use String
here (probably easiest), or make a new immutables type containing the string prefix and immutables will generate hashCode()
and equals()
for us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I switched back to using the concatenation of gitDir path and prefix as the key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do not use the gitDir as part of the key, we'd fail the test subproject version
test where the two projects have different worktree.
94ba4c8
to
8915a4c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
Before this PR
gitVersion()
orversionDetails()
does not have caching, every time it is called, it will create a new 'VersionDetails' object and call the expensivegit describe
operation to get the version. This causes long configuration time in a lot of the repos whereversion gitVersion()
is called under allprojects.After this PR
Build service is used so if we call
gitVersion()
on all projects, we will not call the expensivegit describe
many times. Instead, the build service will have a concurrent hash map that stores the createdVersionDetails
object with a specific prefix as key the first time it is called. IfgitVersion()
is previously computed, it will return the cached value the next time it is called.Possible downsides?
Build service is only supported from Gradle 6. This means this plugin will no longer support Gradle 5.