This chapter provides an introduction to "configuration avoidance" when dealing with tasks and explains some guidelines for migrating your build to use the configuration avoidance APIs effectively. The API described here co-exists with an existing API that will be replaced with our usual deprecation process over several major releases. As of Gradle 5.1, we recommend that the configuration avoidance APIs be used whenever tasks are created by custom plugins.

How does the configuration avoidance API work?

In a nutshell, the API allows builds to avoid the cost of creating and configuring tasks during Gradle’s configuration phase when those tasks will never be executed. For example, when running a compile task, other unrelated tasks, like code quality, testing and publishing tasks, will not be executed, so any time spent creating and configuring those tasks is unnecessary. The configuration avoidance API avoids configuring tasks if they will not needed during the course of a build, which can have a significant impact on total configuration time.

To avoid creating and configuring tasks, we say that a task is "registered" but not created. When a task is in this state, 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 actually been created, and none of its configuration actions have been executed. It will remain in this state until something in the build needs the instantiated task object (for instance if the task is executed on the command line or the task is a dependency of a task executed on the command line). If the task object is never needed, then the task will remain in the registered state, and the cost of creating and configuring the task will be avoided.

In Gradle, you register a task using TaskContainer.register(java.lang.String). There are variations of this method that allow providing a task type and/or an action for modifying the task configuration. 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 (for example when creating task dependencies).

Guidelines

How do I 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 it is 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, as explained in the following sections.

How do I 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 tasks. To defer task configuration, you will need to migrate to a configuration avoidance API equivalent. See the table below to identify the alternative.

How do I reference a task without creating/configuring it?

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 when calling TaskContainer.register(java.lang.String) or using 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 in the same way as Task, so you do not need to unwrap a Provider for explicit dependencies to continue to work.

If you are configuring a task by name, you will need to use the configuration avoidance equivalent. See the table below to identify the alternative.

How to get an instance of a Task?

In the event you still need to get access to a Task instance, you can use TaskCollection.named(java.lang.String) and call Provider.get(). This will cause the task to be created/configured, but everything should work as it has with the eager APIs.

Migration Guide

The following sections will go through some general guidelines to adhere to when migrating the build logic as well as the steps we recommend following. We also cover some troubleshooting and pitfalls to help you work around some issues you may encounter during the migration.

General

  1. Use help task as a benchmark during the migration. The help task is the perfect candidate to benchmark your migration process. In a build that uses only the configuration avoidance API, a build scan would show no tasks created immediately or created during configuration, and only the tasks that were actually executed would be created. Be mindful of the version of the build scan plugin in use.

  2. 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:

    def check = tasks.register("check")
    tasks.register("verificationTask") { verificationTask ->
        // Configure verificationTask
    
        // Run verificationTask when someone runs check
        check.get().dependsOn verificationTask
    }
    val check by tasks.registering
    tasks.register("verificationTask") {
        // Configure verificationTask
    
        // Run verificationTask when someone runs check
        check.get().dependsOn(this)
    }

    Executing the gradle check task should execute verificationTask, but with this example, it won’t. This is because the dependency between verificationTask and check only happens when verificationTask 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. The code would become:

    def check = tasks.register("check")
    def verificationTask = tasks.register("verificationTask") {
        // Configure verificationTask
    }
    check.configure {
        dependsOn verificationTask
    }
    val check by tasks.registering
    val verificationTask by tasks.registering {
        // Configure verificationTask
    }
    check {
        dependsOn(verificationTask)
    }

    In the future, Gradle will consider this sort of anti-pattern an error and will produce an exception.

  3. Prefer small incremental changes. Smaller changes are easier to sanity check. If you ever break your build logic, it will be easier to analyze the changelog since the last successful verification.

  4. Ensure a good plan is established for validating the build logic. Usually, a simple build 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 plan for verification.

  5. Prefer automatic testing to manual testing. It’s good practice to write integration test for your build logic using TestKit.

  6. Avoid referencing a task by name. In the majority of cases, referencing a task by name is a fragile pattern and should be avoided. Although the task name is available on the TaskProvider, effort should be made to use references from a strongly typed model instead.

  7. Use the new task API as much as possible. Eagerly realizing some tasks may cause a cascade of other tasks to be realized. Using TaskProvider helps create an indirection that protects against transitive realization.

  8. 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. Since afterEvaluate is used to delay configuring a Project, 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 an afterEvaluate block may be expected to always 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. The following explores the recommended steps for a successful migration. While going through these steps, keep in mind the guidelines above.

Using the new API in a plugin will require users to use Gradle 4.9 or later. Plugin authors should refer to Supporting older versions of Gradle section.

  1. 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.

  2. Migrate tasks configured by name. Similar to the previous point, this will cause your build to eagerly create fewer tasks that are registered by plugins. For example, logic that uses TaskContainer#getByName(String, Closure/Action) should be converted to TaskContainer#named(String).configure(Closure/Action). This also includes task configuration via DSL blocks.

  3. Migrate tasks creation to register(…​). At this point, you should change anywhere that you are creating tasks to instead register those tasks.

For all steps above, be aware of the common pitfalls around deferred configuration.

After making these changes, you should see improvement in the number of tasks that are eagerly created at configuration time. Use build scans to understand what tasks are still being created eagerly and where this is happening.

Troubleshooting

  • What tasks are being realized? As we keep developing the feature, more reporting, and troubleshooting information will be made available to answer this question. In the meantime, build scan is the best way to answer this question. Follow these steps:

    1. Create a build scan. Execute the Gradle command using the --scan flag.

    2. Navigate to the configuration performance tab.

      taskConfigurationAvoidance navigate to performance
      Figure 1. Navigate to configuration performance tab in build scan
      1. Navigate to the performance card from the left side menu.

      2. Navigate to the configuration tab from the top of the performance card.

    3. All the information requires will be presented.

      taskConfigurationAvoidance performance annotated
      Figure 2. Configuration performance tab in build scan annotated
      1. Total tasks present when each task is created or not.

        • "Created immediately" represents tasks that were created using the eager task APIs.

        • "Created during configuration" represents tasks that were created using the configuration avoidance APIs, but were realized explicitly (via TaskProvider#get()) or implicitly using the eager task query APIs.

        • Both "Created immediately" and "Created during configuration" numbers are considered the "bad" numbers that should be minimized as much as possible.

        • "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 that were avoided in this build session.

      2. 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 that were created either immediately or during configuration. Ideally, this column should be empty.

      3. Focusing on a script, plugin, or lifecycle callback will show a break down of the tasks that were created.

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 immediately be created:

    // 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").configure {
        // ...
        // 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.

      def someTask = tasks.register("someTask")
      
      task anEagerTask {
          dependsOn someTask
      }
      val someTask by tasks.registering
      
      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"))
      }
  • The build scan plugin buildScanPublishPrevious task is eager until version 1.15. Upgrade the build scan plugin in your build to use the latest version.

Supporting older versions of Gradle

This section describes two ways to keep your plugin backward compatible with older version of Gradle if you must maintain compatibility with versions of Gradle older than 4.9. Most of the new API methods are available starting with Gradle 4.9.

Although backward compatibility is good for users, we still recommended to upgrade to newer Gradle releases in a timely manner. This will reduce your maintenance burden.

The first method to maintain compatibility is to compile your plugin against the Gradle 4.9 API and conditionally call the right APIs with Groovy (example).

The second method is to use Java reflection to cope with the fact that the APIs are unavailable during compilation (example).

It is highly recommended to have cross-version test coverage using TestKit and multiple versions of Gradle.

Existing vs New API overview

  • Methods that take a groovy.lang.Closure are covered in the new API with methods taking org.gradle.api.Action.

  • More convenience methods may be added in the future based on user feedback.

  • Some old API methods may never have a direct replacement in the new API.

  • Some APIs may be restricted when accessed in a configuration action registered via the configuration avoidance methods.

Old vs New API Description

Instead of: task myTask(type: MyTask) {}

There is not a shorthand Groovy DSL for using the new API.

Use: tasks.register("myTask", MyTask) {}

Use one of the alternatives below.

Use: No direct equivalent.

Use one of the alternatives below.

Use: No direct equivalent.

This returns a TaskProvider instead of a Task.

This returns a TaskProvider instead of a Task.

This returns a TaskProvider instead of a Task.

This returns a TaskProvider instead of a Task.

This returns a TaskProvider instead of a Task.

This returns a TaskProvider instead of a Task.

This returns a TaskProvider instead of a Task.

Use: named(java.lang.String).configure(Action)

Accessing tasks from another project requires a specific ordering of project evaluation.

Use: No direct equivalent.

named(String) is the closest equivalent, but will fail if the task does not exist. Using findByName(String) will cause tasks registered with the new API to be created/configured.

Use: No direct equivalent.

See getByPath(String) above.

Use: No direct equivalent.

This is OK to use because it does not require tasks to be created immediately.

Use: OK

Instead of: withType(java.lang.Class).getByName(java.lang.String)

This returns a TaskProvider instead of a Task.

Use: named(java.lang.String, java.lang.Class)

This returns void, so it cannot be chained.

Use: withType(java.lang.Class).configureEach(org.gradle.api.Action)

This returns void, so it cannot be chained.

This returns void, so it cannot be chained.

This returns void, so it cannot be chained.

Avoid calling this method. matching(Spec) and configureEach(Action) are more appropriate in most cases.

Use: OK, with issues.

This is OK to use because it does not require tasks to be created immediately.

Use: OK

Avoid calling this directly as it’s a Groovy convenience method. The alternative returns a TaskProvider instead of a Task.

Instead of: iterator() or implicit iteration over the Task collection

Avoid doing this as it requires creating and configuring all tasks. See findAll(Closure) above.

Use: OK, with issues.

Instead of: remove(org.gradle.api.Task)

Avoid calling this. The behavior of remove with the new API may change in the future.

Use: OK, with issues.

Avoid calling this. The behavior of replace with the new API may change in the future.

Use: OK, with issues.

Avoid calling this. The behavior of replace with the new API may change in the future.

Use: OK, with issues.