Avoiding Unnecessary Task Configuration
As of Gradle 5.1, we recommend that the configuration avoidance APIs be used whenever tasks are created.
Task configuration avoidance API
The configuration avoidance API avoids configuring tasks if they will not be used for a build, which can significantly impact total configuration time.
For example, when running a compile
task (with the java
plugin applied), other unrelated tasks (such as clean
, test
, javadocs
), will not be executed.
To avoid creating and configuring a task not needed for a build, we can register that task instead.
When a task is registered, it is known to the build. It can be configured, and references to it can be passed around, but the task object itself has not been created, and its actions have not been executed. The registered task will remain in this state until something in the build needs the instantiated task object. If the task object is never needed, the task will remain registered, and the cost of creating and configuring the task will be avoided.
In Gradle, you register a task using TaskContainer.register(java.lang.String).
Instead of returning a task instance, the register(…)
method returns a TaskProvider, which is a reference to the task that can be used in many places where a normal task object might be used (i.e., when creating task dependencies).
Guidelines
Defer task creation
Effective task configuration avoidance requires build authors to change instances of TaskContainer.create(java.lang.String) to TaskContainer.register(java.lang.String).
Older versions of Gradle only support the create(…)
API.
The create(…)
API eagerly creates and configures tasks when called and should be avoided.
Using register(…) alone may not be enough to avoid all task configuration completely.
You may need to change other code that configures tasks by name or by type, see below.
|
Defer task configuration
Eager APIs like DomainObjectCollection.all(org.gradle.api.Action) and DomainObjectCollection.withType(java.lang.Class, org.gradle.api.Action) will immediately create and configure any registered task. To defer task configuration, you must migrate to a configuration avoidance API equivalent. See the table below to identify the best alternative.
Reference a registered task
Instead of referencing a task object, you can work with a registered task via a TaskProvider object. A TaskProvider can be obtained in several ways including the TaskContainer.register(java.lang.String) and the TaskCollection.named(java.lang.String) method.
Calling Provider.get() or looking up a task by name with TaskCollection.getByName(java.lang.String) will cause the task to be created and configured.
Methods like Task.dependsOn(java.lang.Object…) and ConfigurableFileCollection.builtBy(java.lang.Object...) work with TaskProvider the same way as Task so you do not need to unwrap a Provider
for explicit dependencies to continue to work.
You must use the configuration avoidance equivalent to configure a task by name. See the table below to identify the best alternative.
Reference a task instance
In the event you need to get access to a Task instance, you can use TaskCollection.named(java.lang.String) and Provider.get(). This will cause the task to be created and configured, but everything should work as it had with the eager APIs.
Task ordering with configuration avoidance
Calling ordering methods will not cause task creation by itself. All these methods do is declare relationships.
The existence of these relationships might indirectly cause task creation in later stages of the build process. |
When task relationships need to be established (i.e., dependsOn
, finalizedBy
, mustRunAfter
, shouldRunAfter
), a distinction can be made between soft and strong relationships.
Their effects on task creation during the configuration phase differ:
-
Task.mustRunAfter(…) and Task.shouldRunAfter(…) represent soft relationships, which can only change the order of existing tasks, but can’t trigger their creation.
-
Task.dependsOn(…) and Task.finalizedBy(…) represent strong relationships, which force the execution of referenced tasks, even if they hadn’t been created otherwise.
-
If a task is not executed, regardless if it was created with Task.register(…) or Task.create(…), the defined relationships will not trigger task creation at configuration time.
-
If a task is executed, all strongly associated tasks must be created and configured at configuration time, as they might have other
dependsOn
orfinalizedBy
relationships. This will happen transitively until the task graph contains all strong relationships.
Migration guide
The following sections will go through some general guidelines to adhere to when migrating your build logic. We’ve also provided some recommended steps to follow along with troubleshooting and common pitfalls.
Migration guidelines
-
Use
help
task as a benchmark during the migration.
Thehelp
task is the perfect candidate to benchmark your migration process. In a build that uses only the configuration avoidance API, a build scan shows no tasks created during configuration, and only the tasks executed are created. -
Only mutate the current task inside a configuration action.
Because the task configuration action can now run immediately, later, or never, mutating anything other than the current task can cause indeterminate behavior in your build. Consider the following code:val check by tasks.registering tasks.register("verificationTask") { // Configure verificationTask // Run verificationTask when someone runs check check.get().dependsOn(this) }
def check = tasks.register("check") tasks.register("verificationTask") { verificationTask -> // Configure verificationTask // Run verificationTask when someone runs check check.get().dependsOn verificationTask }
Executing the
gradle check
task should executeverificationTask
, but with this example, it won’t. This is because the dependency betweenverificationTask
andcheck
only happens whenverificationTask
is realized. To avoid issues like this, you must only modify the task associated with the configuration action. Other tasks should be modified in their own configuration action:val check by tasks.registering val verificationTask by tasks.registering { // Configure verificationTask } check { dependsOn(verificationTask) }
def check = tasks.register("check") def verificationTask = tasks.register("verificationTask") { // Configure verificationTask } check.configure { dependsOn verificationTask }
In the future, Gradle will consider this sort of antipattern an error and produce an exception.
-
Prefer small incremental changes.
Smaller changes are easier to sanity check. If you ever break your build logic, analyzing the changelog since the last successful verification will be easier. -
Ensure a good plan is established for validating the build logic.
Usually, a simplebuild
task invocation should do the trick to validate your build logic. However, some builds may need additional verification — understand the behavior of your build and make sure you have a good verification plan. -
Prefer automatic testing to manual testing.
Writing integration test for your build logic using TestKit is good practice. -
Avoid referencing a task by name.
Usually, referencing a task by name is a fragile pattern and should be avoided. Although the task name is available on theTaskProvider
, an effort should be made to use references from a strongly typed model instead. -
Use the new task API as much as possible.
Eagerly realizing some tasks may cause a cascade of other tasks to be realized. UsingTaskProvider
helps create an indirection that protects against transitive realization. -
Some APIs may be disallowed if you try to access them from the new API’s configuration blocks.
For example,Project.afterEvaluate()
cannot be called when configuring a task registered with the new API. SinceafterEvaluate
is used to delay configuring aProject
, mixing delayed configuration with the new API can cause errors that are hard to diagnose because tasks registered with the new API are not always configured, but anafterEvaluate
block may always be expected to execute.
Migration steps
The first part of the migration process is to go through the code and manually migrate eager task creation and configuration to use configuration avoidance APIs.
-
Migrate task configuration that affects all tasks (
tasks.all {}
) or subsets by type (tasks.withType(…) {}
).
This will cause your build to eagerly create fewer tasks that are registered by plugins. -
Migrate tasks configured by name.
This will cause your build to eagerly create fewer tasks that are registered by plugins. For example, logic that usesTaskContainer#getByName(String, Closure)
should be converted toTaskContainer#named(String, Action)
. This also includes task configuration via DSL blocks. -
Migrate tasks creation to
register(…)
.
At this point, you should change any task creation (usingcreate(…)
or similar) to use register instead.
After making these changes, you should see an improvement in the number of tasks eagerly created at configuration time.
Migration troubleshooting
-
What tasks are being realized? Use a Build Scan to troubleshoot by following these steps:
-
Execute the Gradle command using the
--scan
flag. -
Navigate to the configuration performance tab:
-
All the information required will be presented:
-
Total tasks present when each task is created or not.
-
Created immediately
represents tasks created using the eager task APIs. -
Created during configuration
represents tasks created using the configuration avoidance APIs, but were realized explicitly (viaTaskProvider#get()
) or implicitly using the eager task query APIs. -
Both
Created immediately
andCreated during configuration
numbers are considered "bad" numbers that should be minimized as much as possible. -
Created during task execution
represents the tasks created after the task graph has been created. Any tasks created at this point won’t be executed as part of the graph. Ideally, this number should be zero. -
Created during task graph calculation
represents the tasks created when building the execution task graph. Ideally, this number would be equal to the number of tasks executed. -
Not created
represents the tasks avoided in this build session. Ideally, this number is as large as possible.
-
-
The next section helps answer the question of where a task was realized. For each script, plugin, or lifecycle callback, the last column represents the tasks created immediately or during configuration. Ideally, this column should be empty.
-
Focusing on a script, plugin, or lifecycle callback will show a breakdown of the tasks that were created.
-
-
Migration pitfalls
-
Beware of the hidden eager task realization. There are many ways that a task can be configured eagerly.
For example, configuring a task using the task name and a DSL block will cause the task to be created (when using the Groovy DSL) immediately:// Given a task lazily created with tasks.register("someTask") // Some time later, the task is configured using a DSL block someTask { // This causes the task to be created and this configuration to be executed immediately }
Instead use the
named()
method to acquire a reference to the task and configure it:tasks.named("someTask") { // ... // Beware of the pitfalls here }
Similarly, Gradle has syntactic sugar that allows tasks to be referenced by name without an explicit query method. This can also cause the task to be immediately created:
tasks.register("someTask") // Sometime later, an eager task is configured like task anEagerTask { // The following will cause "someTask" to be looked up and immediately created dependsOn someTask }
There are several ways this premature creation can be avoided:
-
Use a
TaskProvider
variable. Useful when the task is referenced multiple times in the same build script.val someTask by tasks.registering task("anEagerTask") { dependsOn(someTask) }
def someTask = tasks.register("someTask") task anEagerTask { dependsOn someTask }
-
Migrate the consumer task to the new API.
tasks.register("someTask") tasks.register("anEagerTask") { dependsOn someTask }
-
Lookup the task lazily. Useful when the tasks are not created by the same plugin.
tasks.register("someTask") task("anEagerTask") { dependsOn(tasks.named("someTask")) }
tasks.register("someTask") task anEagerTask { dependsOn tasks.named("someTask") }
-
Lazy APIs to use
API | Note |
---|---|
Returns a |
|
Returns a |
|
Ok to use. If chained |
|
Returns |
Eager APIs to avoid
API | Note |
---|---|
|
Do not use the shorthand notation. Use |
Use |
|
Do not use. |
|
Do not use. |
|
Avoid calling this. The behavior may change in the future. |
|
Use |
|
Use |
|
Use |
|
If you are matching based on the name, use |
|
Use |
|
Use |
|
Use |
|
Use |
|
Avoid calling this method. |
|
Do not use. |
|
|
Avoid doing this as it requires creating and configuring all tasks. |
|
Avoid calling this. The behavior may change in the future. |