How the Micronaut team leverages Gradle’s version catalogs for improved developer productivity

12 March 2023

Tags: micronaut gradle version catalogs graalvm maven

This blog post discusses how the Micronaut development team makes use of a feature of Gradle, version catalogs, to improve the team’s developer productivity, reduce the risks of publishing broken releases, coordinate the releases of a large number of modules and, last but not least, provide additional features to our Gradle users.

The backstory

The Micronaut Framework is a modern open-source framework for building JVM applications. It can be used to build all kinds of applications, from CLI applications to microservices or even good old monoliths. It supports deploying both to the JVM and native executables (using GraalVM), making it particularly suitable for all kind of environments. A key feature of the Micronaut framework is developer productivity: we do everything we can to make things faster for developers. In particular, Micronaut has a strong emphasis on easing how you test your applications, even in native mode. For this we have built a number of tools, including our Maven and Gradle plugins.

When I joined the Micronaut team almost a couple years back, I was given the responsibility of improving the team’s own developer productivity. It was an exciting assignment, not only because I knew the team’s love about Gradle, but because I also knew that there were many things we could do to reduce the feedback time, to provide more insights about failures, to detect flaky tests, etc. As part of this work we have put in place a partnership with Gradle Inc which kindly provides us with a Gradle Enterprise instance, but this is not what I want to talk about today.

Lately I was listening to an interview of Aurimas Liutikas of the AndroidX team, who was saying that he didn’t think that version catalogs were a good solution for library authors to share their recommendations of versions, and that BOMs are probably a better solution for this. I pinged him saying that I disagreed with this statement and offered to provide more details why, if he was interested. This is therefore a long answer, but one which will be easier to find than a thread on social media.

What are version catalogs?

Let’s start with the basics: a version catalog is, like the name implies, a catalog of versions to pick from, nothing more. That doesn’t sound too much exciting, and what versions are we talking about? That’s version of libraries or plugins that you use in your build.

As an illustration, here is a version catalog, defined as a TOML file:

[versions]
javapoet = "1.13.0"

[libraries]
javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" }

Then this library can be used in a dependencies declaration block in any of the project’s build script using a type-safe notation:

dependencies {
    implementation(libs.javapoet) {
        because("required for Java source code generation")
    }
}

which is strictly equivalent to writing:

dependencies {
    implementation("com.squareup:javapoet:1.13.0") {
        because("required for Java source code generation")
    }
}

There are many advantages of using version catalogs to declare your library versions, but most notably it provides a single, standard location where those versions are declared. It is important to understand that a catalog is simply a list of dependencies you can pick from, a bit like going to the supermarket and choosing whatever you need for your particular meal: it’s not because a catalog declares libraries that you have to use them. However, a catalog provides you with recommendations of libraries to pick from.

Version catalogs for Micronaut users

An interesting aspect of version catalogs is that they can be published, for others to consume: they are an artifact. Micronaut users can already make use of catalogs, as I have explained in a previous blog post. This makes it possible for a user who doesn’t know which version of Micronaut Data to use, to simply declare:

dependencies {
    implementation mn.micronaut.data
}

People familiar with Maven BOMs can easily think that it is the same feature, but there are key differences which are described in the Gradle docs.

In the rest of this post we will now focus on how we generate those catalogs, and how they effectively help us in improving our own developer productivity.

How the Micronaut team uses version catalogs

One catalog per module

As I said, the Micronaut framework consists of a large number of modules which live in their own Git repository. All the projects share the same layout, the same conventions in order to make things easier to maintain. For this purpose, we use our own collection of internal build plugins as well as a project template.

Those build plugins provide features like:

  • defining the default Java language level, setting up code conventions and code quality plugins

  • standardizing how documentation is built (using Asciidoctor)

  • setting up integration with Gradle Enterprise, to publish build scans, configure the build cache and predictive test selection

  • implementing binary compatibility checks between releases

  • configuring publication to Maven Central

  • providing a high-level model of what a Micronaut module is

The last item is particularly important: in every Micronaut project, we have different kind of modules: libraries (which are published to Maven Central for users to consume), internal support libraries (which are not intended for external consumption), or a BOM module (which also publishes a version catalog as we’re going to see).

Long story short: we heavily rely on conventions to reduce the maintenance costs, have consistent builds, with improved performance and higher quality standards. If you are interested in why we have such plugins, Sergio Delamo and I gave an interview about this a few months ago (alert: the thumbnail shows I have hair, this is fake news!).

Each of our projects declares a version catalog, for example:

Automatic version upgrades

One of the advantages of version catalogs is that it provides a centralized place for versions, which can be easily used by bots to provide pull requests for dependency upgrades. For this, we use Renovatebot which integrates particularly well with version catalogs (GitHub’s dependabot lacks behind in terms of support). This allows us to get pull requests like this one which are very easy to review.

BOM and version catalog generation

Each of the Micronaut projects is now required to provide a BOM (Bill of Materials) for users. Another term for a BOM that is used in the Gradle ecosystem is a platform: a platform has however slightly different semantics in Maven and Gradle. The main goal of a BOM is to provide a list of dependencies a project works with, and, in Maven, it can be used to override the dependency versions of transitive dependencies. While in Maven, a BOM will only influence the dependency resolution of the project which imports the BOM, in Gradle a platform fully participates in dependency resolution, including when a transitive dependency depends on a a BOM. To simplify, a user who imports a BOM may use dependencies declared in the BOM without specifying a version: the version will be fetched from the BOM. In that regards, it looks exactly the same as a version catalog, but there are subtle differences.

For example, if a user imports a BOM, any transitive dependency matching a dependency found in the BOM will be overridden (Maven) or participate in conflict resolution (Gradle). That is not the case for a catalog: it will not influence the dependency resolution unless you explicitly add a dependency which belongs to the catalog.

That’s why Micronaut publishes both a BOM and a catalog, because they address different use cases, and they work particularly well when combined together.

In Micronaut modules, you will systematically find a project with the -bom suffix. For example, Micronaut Security will have subprojects like micronaut-security-jwt, micronaut-security-oauth2 and micronaut-security-bom.

The BOM project will aggregate dependencies used by the different modules. In order to publish a BOM file, the only thing a project has to do is to apply our convention plugin:

plugins {
    id "io.micronaut.build.internal.bom"
}

Note how we don’t have to declare the coordinates of the BOM (group, artifact, version), nor that we have to declare how to publish to Maven Central, what dependencies should be included in the BOM, etc: everything is done by convention, that’s the magic of composition over inheritance.

Should we want to change how we generate the BOM, the only thing we would have to do is to update our internal convention plugin, then all projects would benefit from the change once they upgrade.

Convention over configuration

In order to determine which dependencies should be included in our BOM, we defined conventions that we use in our catalog files. In our internal terminology, when we want a dependency to be handled by the Micronaut framework, we call that a managed dependency: a dependency that is managed by Micronaut and that users shouldn’t care about in most cases: they don’t have to think about a version, we will provide one for them.

This directly translates to a convention in the version catalogs of the Micronaut projects: dependencies which are managed need to be declared with a managed- prefix in the catalog:

[versions]
...
managed-kafka = '3.4.0'
...
zipkin-brave-kafka-clients = '5.15.0'

[libraries]
...
managed-kafka-clients = { module = 'org.apache.kafka:kafka-clients', version.ref = 'managed-kafka' }
managed-kafka-streams = { module = 'org.apache.kafka:kafka-streams', version.ref = 'managed-kafka' }
...
zipkin-brave-kafka-clients = { module = 'io.zipkin.brave:brave-instrumentation-kafka-clients', version.ref = 'zipkin-brave-kafka-clients' }

Those dependencies will end up in the version catalog that we generate, but without the managed- prefix. This means that we would generate a BOM which contains the following:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!-- This module was also published with a richer model, Gradle metadata,  -->
  <!-- which should be used instead. Do not delete the following line which  -->
  <!-- is to indicate to Gradle or any Gradle module metadata file consumer  -->
  <!-- that they should prefer consuming it instead. -->
  <!-- do_not_remove: published-with-gradle-metadata -->
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.micronaut.kafka</groupId>
  <artifactId>micronaut-kafka-bom</artifactId>
  <version>5.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <name>Micronaut Kafka</name>
  <description>Integration between Micronaut and Kafka Messaging</description>
  <url>https://micronaut.io</url>
  <licenses>
    <license>
      <name>The Apache Software License, Version 2.0</name>
      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
      <distribution>repo</distribution>
    </license>
  </licenses>
  <scm>
    <url>scm:git@github.com:micronaut-projects/micronaut-kafka.git</url>
    <connection>scm:git@github.com:micronaut-projects/micronaut-kafka.git</connection>
    <developerConnection>scm:git@github.com:micronaut-projects/micronaut-kafka.git</developerConnection>
  </scm>
  <developers>
    <developer>
      <id>graemerocher</id>
      <name>Graeme Rocher</name>
    </developer>
  </developers>
  <properties>
    <micronaut.kafka.version>5.0.0-SNAPSHOT</micronaut.kafka.version>
    <kafka.version>3.4.0</kafka.version>
  </properties>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-clients</artifactId>
        <version>${kafka.compat.version}</version>
      </dependency>
      <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-streams</artifactId>
        <version>${kafka.version}</version>
      </dependency>
      <dependency>
        <groupId>io.micronaut.kafka</groupId>
        <artifactId>micronaut-kafka</artifactId>
        <version>${micronaut.kafka.version}</version>
      </dependency>
      <dependency>
        <groupId>io.micronaut.kafka</groupId>
        <artifactId>micronaut-kafka-streams</artifactId>
        <version>${micronaut.kafka.version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
</project>

Note how we automatically translated the managed-kafka property into a BOM property kafka.version, which is used in the <dependencyManagement> block. Dependencies which do not start with managed- are not included in our generated BOM.

Let’s now look at the version catalog that we generate:

#
# This file has been generated by Gradle and is intended to be consumed by Gradle
#
[metadata]
format.version = "1.1"

[versions]
kafka = "3.4.0"
kafka-compat = "3.4.0"
micronaut-kafka = "5.0.0-SNAPSHOT"

[libraries]
kafka = {group = "org.apache.kafka", name = "kafka-clients", version.ref = "kafka-compat" }
kafka-clients = {group = "org.apache.kafka", name = "kafka-clients", version.ref = "kafka" }
kafka-streams = {group = "org.apache.kafka", name = "kafka-streams", version.ref = "kafka" }
micronaut-kafka = {group = "io.micronaut.kafka", name = "micronaut-kafka", version.ref = "micronaut-kafka" }
micronaut-kafka-bom = {group = "io.micronaut.kafka", name = "micronaut-kafka-bom", version.ref = "micronaut-kafka" }
micronaut-kafka-streams = {group = "io.micronaut.kafka", name = "micronaut-kafka-streams", version.ref = "micronaut-kafka" }

Given a single input, the version catalog that we use to build our Micronaut module, our build conventions let us automatically declare which dependencies should land in the output BOM and version catalogs that we generate for that project! Unlike Maven BOMs which either have to be a parent POM or redeclare all dependencies in an independent module, in Gradle we can generate these automatically and completely decouple the output BOM from what is required to build our project.

In general, all api dependencies must be managed, so in the example above, the Micronaut Kafka build scripts would have an API dependency on kafka-clients, which we can find in the main project build script:

dependencies {
    api libs.managed.kafka.clients
    ...
}

The benefit of generating a version catalog for a user is that there is now a Micronaut Kafka version catalog published on Maven Central, alongside the BOM file.

This catalog can be imported by a user in their settings file:

settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
         create("mnKafka") {
             from("io.micronaut.kafka:micronaut-kafka-bom:4.5.2")
         }
    }
}

Then the dependency on Micronaut Kafka and its managed dependencies can be used in a build script using the mnKafka prefix:

build.gradle
dependencies {
    implementation mnKafka.micronaut.kafka
    implementation mnKafka.kafka.clients
}

A user doesn’t have to know about the dependency coordinates of Kafka clients: the IDE (at least IntelliJ IDEA) would provide completion automatically!

BOM composition

In Micronaut 3.x, there is a problem that we intend to fix in Micronaut 4 regarding our "main" BOM: the Micronaut core BOM is considered as our "platform" BOM, in the sense that it aggregates BOMs of various Micronaut modules. This makes it hard to release newer versions of Micronaut which, for example, only upgrade particular modules of Micronaut.

Therefore in Micronaut 4, we are cleanly separating the "core" BOM, from the new platform BOM. It is interesting in this blog post because it offers us the opportunity to show how we are capable of generating aggregating BOMs and aggregated catalogs.

In the platform BOM module, you can find the "input" catalog that we use, and only consists of managed- dependencies. Most of those dependencies are simply dependencies on other Micronaut BOMs: this is an "aggregating" BOM, which imports other BOMs. This is, therefore, the only BOM that a user would effectively have to use when migrating to Micronaut 4: instead of importing all BOMs for the different Micronaut modules they use, they can simply import the Micronaut Platform BOM, which will then automatically include the BOMs of other modules which "work well together".

This allows us to decouple the releases of the framework from the releases of Micronaut core itself.

However, there is a subtlety about aggregating BOMs in Maven: they are not regular dependencies, but dependencies with the import scope. This means that we must make a difference between a "managed dependency" and an "imported BOM" in our input catalog.

To do this, we have another naming convention, which is to use the boms- prefix for imported BOMs:

[versions]
...
managed-micronaut-aws = "4.0.0-SNAPSHOT"
managed-micronaut-azure = "5.0.0-SNAPSHOT"
managed-micronaut-cache = "4.0.0-SNAPSHOT"
managed-micronaut-core = "4.0.0-SNAPSHOT"
...

[libraries]
...
boms-micronaut-aws = { module = "io.micronaut.aws:micronaut-aws-bom", version.ref = "managed-micronaut-aws" }
boms-micronaut-azure = { module = "io.micronaut.azure:micronaut-azure-bom", version.ref = "managed-micronaut-azure" }
boms-micronaut-cache = { module = "io.micronaut.cache:micronaut-cache-bom", version.ref = "managed-micronaut-cache" }
boms-micronaut-core = { module = "io.micronaut:micronaut-core-bom", version.ref = "managed-micronaut-core" }
...

This results in the following BOM file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.micronaut.platform</groupId>
  <artifactId>micronaut-platform</artifactId>
  <version>4.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <name>Micronaut Platform</name>
  <description>Bill-Of-Materials (BOM) and Gradle version catalogs for Micronaut</description>

  ...

  <properties>
    ...
    <micronaut.aws.version>4.0.0-SNAPSHOT</micronaut.aws.version>
    <micronaut.azure.version>5.0.0-SNAPSHOT</micronaut.azure.version>
    <micronaut.cache.version>4.0.0-SNAPSHOT</micronaut.cache.version>
    <micronaut.core.version>4.0.0-SNAPSHOT</micronaut.core.version>
    ...
  </properties>
  <dependencyManagement>
    <dependencies>
      ...
      <dependency>
        <groupId>io.micronaut.aws</groupId>
        <artifactId>micronaut-aws-bom</artifactId>
        <version>${micronaut.aws.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>io.micronaut.azure</groupId>
        <artifactId>micronaut-azure-bom</artifactId>
        <version>${micronaut.azure.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>io.micronaut.cache</groupId>
        <artifactId>micronaut-cache-bom</artifactId>
        <version>${micronaut.cache.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-core-bom</artifactId>
        <version>${micronaut.core.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      ...
    </dependencies>
  </dependencyManagement>
</project>

A more interesting topic to discuss is what we can do with version catalogs that we publish for users: we can inline dependency aliases from each of the imported catalogs into the platform catalog. All dependencies in the catalog files of each modules are directly available in the platform catalog:

[versions]
dekorate = "1.0.3"
elasticsearch = "7.17.8"
...
micronaut-aws = "4.0.0-SNAPSHOT"
micronaut-azure = "5.0.0-SNAPSHOT"
micronaut-cache = "4.0.0-SNAPSHOT"
micronaut-core = "4.0.0-SNAPSHOT"
...

[libraries]
alexa-ask-sdk = {group = "com.amazon.alexa", name = "ask-sdk", version = "" }
alexa-ask-sdk-core = {group = "com.amazon.alexa", name = "ask-sdk-core", version = "" }
alexa-ask-sdk-lambda = {group = "com.amazon.alexa", name = "ask-sdk-lambda-support", version = "" }
aws-java-sdk-core = {group = "com.amazonaws", name = "aws-java-sdk-core", version = "" }
aws-lambda-core = {group = "com.amazonaws", name = "aws-lambda-java-core", version = "" }
aws-lambda-events = {group = "com.amazonaws", name = "aws-lambda-java-events", version = "" }
aws-serverless-core = {group = "com.amazonaws.serverless", name = "aws-serverless-java-container-core", version = "" }
awssdk-secretsmanager = {group = "software.amazon.awssdk", name = "secretsmanager", version = "" }
azure-cosmos = {group = "com.azure", name = "azure-cosmos", version = "" }
azure-functions-java-library = {group = "com.microsoft.azure.functions", name = "azure-functions-java-library", version = "" }
...

The alexa-ask-sdk is for example an alias which was originally declared in the micronaut-aws module. Because we aggregate all catalogs, we can inline those aliases and make them directly available in user build scripts:

settings.gradle
dependencyResolutionManagement {
    versionCatalogs {
         create("mnKafka") {
             from("io.micronaut.platform:micronaut-platform:4.0.0-SNAPSHOT")
         }
    }
}
build.gradle
dependencies {
...
    implementation(mn.micronaut.aws.alexa)
    implementation(mn.alexa.sdk)
}

Generating a version catalog offers us a very pragmatic way to define all dependencies that users can use in their build scripts with guarantees that they work well together.

Technical details

If you survived reading up to this point, you may be interested in learning how, technically, we implemented this. You can take a look at our internal build plugins, but more specifically at the BOM plugin.

In order to generate our BOM and version catalogs, we have mainly 2 inputs:

  1. the list of subprojects which need to participate in the BOM: in a Micronaut modules, we explained that we have several kinds of projects: libraries which are published, test suites, etc. Only a subset of these need to belong to the BOM, and we can determine that list automatically because each project applies a convention plugin which determines its kind. Only projects of a particular kind are included. Should exceptions be required, we have a MicronautBomExtension which allows us to configure more precisely what to include or not, via a nice DSL.

  2. the list of dependencies, which is determined from the project’s version catalog

One issue is that while Gradle provides automatically the generated, type-safe accessors for version catalogs, there is actually no built-in model that you can access to represent the catalog model itself (what is an alias, references to versions, etc): the type-safe API represents a "realized" catalog, but not a low-level model that we can easily manipulate. This means that we had to implement our own model for this.

We have also seen that we can generate a single platform, aggregating all Micronaut modules for a release, that the users can import into their build scripts. Unfortunately it is not the case for the Micronaut modules themselves: for example, Micronaut Core must not depend on other Micronaut modules, but, for example, Micronaut Data can depend on Micronaut SQL and use dependencies from the Micronaut SQL catalog. Those modules cannot depend on the platform BOM, because this is the aggregating BOM, so we would create a cyclic dependency and wouldn’t be able to release any module.

To mitigate this problem, our internal build plugins expose a DSL which allows each projects to declare which other modules they use:

settings.gradle
micronautBuild {
    importMicronautCatalog() // exposes a `mn` catalog
    importMicronautCatalog("micronaut-reactor") // exposes a `mnReactor` catalog
    importMicronautCatalog("micronaut-rxjava2") // exposes a `mnRxjava2` catalog
    ...
}

While this is simple from the declaration site point of view, it is less practical from a consuming point of view, since it forces us to use different namespaces for each imported catalog:

dependencies {
    ...
    testImplementation mn.micronaut.inject.groovy
    testImplementation mnRxjava2.micronaut.rxjava2
    ...
}

It would have been better if we could actually merge several catalogs into a single one, but unfortunately that feature has been removed from Gradle. I still have hope that this will eventually be implemented, because not having this creates unnecessary boilerplate in build scripts and redundancy in names (e.g implementation mnValidation.micronaut.validation).

Additional benefits and conclusion

All that I described in this article aren’t the only benefits that we have on standardizing on version catalogs. For example, we have tasks which allow us to check that our generated BOM files only reference dependencies which are actually published on Maven Central, or that there are no SNAPSHOT dependencies when we perform a release. In the end, while most of the Micronaut developers had no idea what a version catalog was when I joined the team, all of them pro-actively migrated projects to use them because, I think, they immediately saw the benefits and value. It also streamlined the dependency upgrade process which was still a bit cumbersome before, despite using dependabot.

We now have a very pragmatic way to both use catalogs for building our own projects, and generating BOMs and version catalogs which can be used by both our Maven and Gradle users. Of course, only the Gradle users will benefit from the version catalogs, but we did that in a way which doesn’t affect our Maven users (and if you use Maven, I strongly encourage you to evaluate building Micronaut projects with Gradle instead, since the UX is much better).

I cannot end this blog post without mentioning a "problem" that we have today, which is that if you use Micronaut Launch to generate a Micronaut project, then it will not use version catalogs. We have an issue for this and pull requests are very welcome!

Gradle’s flexibility in action

06 February 2023

Tags: gradle micronaut

I often say that flexibility isn’t the reason why you should select Gradle to build your projects: reliability, performance, reproducibility, testability are better reasons. There are, however, cases were its flexibility comes in handy, like last week, when a colleague of mine asked me how we could benchmark a Micronaut project using a variety of combination of features and Java versions. For example, he wanted to compare the performance of an application with and without epoll enabled, with and without Netty’s tcnative library, with and without loom support, building both the fat jar and native binary, etc. Depending on the combinations, the dependencies of the project may be a little different, or the build configuration may be a little different.

It was an interesting challenge to pick up and the solution turned out to be quite elegant and very powerful.

Conceptual design

I have tried several options before this one, which I’m going to explain below, but let’s focus with the final design (at least at the moment I write this blog post). The matrix of artifacts to be generated can be configured in the settings.gradle file:

combinations {
    dimension("tcnative") {   (1)
        variant("off")
        variant("on")
    }
    dimension("epoll") {      (2)
        variant("off")
        variant("on")
    }
    dimension("json") {       (3)
        variant("jackson")
        variant("serde")
    }
    dimension("micronaut") {  (4)
        variant("3.8")
        variant("4.0")
    }
    dimension("java") {       (5)
        variant("11")
        variant("17")
    }
    exclude {                 (6)
        // Combination of Micronaut 4 and Java 11 is invalid
        it.contains("micronaut-4.0") && it.contains("java-11")
    }
}
1 a dimension called tcnative is defined with 2 variants, on and off
2 another dimension called epool also has on and off variants
3 the json dimension will let us choose 2 different serialization frameworks: Jackson or Micronaut Serde
4 we can also select the version of Micronaut we want to test
5 as well as the Java version!
6 some invalid combinations can be excluded

The generates a number of synthetic Gradle projects, that is to say "projects" in the Gradle terminology, but without actually duplicating sources and directories on disk. With the example above, we generate the following projects:

  • :test-case:tcnative-off:epoll-off:json-jackson:micronaut-3.8:java-11

  • :test-case:tcnative-off:epoll-off:json-jackson:micronaut-3.8:java-17

  • :test-case:tcnative-off:epoll-off:json-jackson:micronaut-4.0:java-17

  • :test-case:tcnative-off:epoll-off:json-serde:micronaut-3.8:java-11

  • :test-case:tcnative-off:epoll-off:json-serde:micronaut-3.8:java-17

  • :test-case:tcnative-off:epoll-off:json-serde:micronaut-4.0:java-17

  • :test-case:tcnative-off:epoll-on:json-jackson:micronaut-3.8:java-11

  • :test-case:tcnative-off:epoll-on:json-jackson:micronaut-3.8:java-17

  • :test-case:tcnative-off:epoll-on:json-jackson:micronaut-4.0:java-17

  • …​ and more

To build the fat jar of the "tcnative on", "epoll on", "Jackson", "Micronaut 4.0" on Java 17 combination, you can invoke:

$ ./gradlew :test-case:tcnative-on:epoll-on:json-jackson:micronaut-4.0:java-17:shadowJar

And building the native image of the "tcnative off", "epoll on", "Micronaut Serde", "Micronaut 3.8" on Java 17 combination can be done with:

$ ./gradlew :test-case:tcnative-off:epoll-on:json-serde:micronaut-3.8:java-17:nativeCompile

Cherry on the cake, all variants can be built in parallel by executing either ./gradlew shadowJar (for the fat jars) or ./gradlew nativeCompile (for the native binaries), which would copy all the artifacts under the root projects build directory so that they are easy to find in a single place.

How does it work?

In a typical project, say the Micronaut application we want to benchmark, you would have a project build which consists of a single Micronaut application module. For example, running ./gradlew build would build that single project artifacts. In a multi-project build, you could have several modules, for example core and app, and running :core:build would only build the core library and :app:build would build both core and app (assuming app depends on core. In both cases, single or multi-project builds, for a typical Gradle project, there’s a real directory associated for each project core, app, etc, where we can find sources, resources, build scripts, etc.

For synthetic projects, we actually generate Gradle projects (aka modules) programmatically. We have a skeleton directory, called test-case-common, which actually defines our application sources, configuration files, etc. It also contains a build script which applies a single convention plugin, named io.micronaut.testcase. This plugin basically corresponds to our "baseline" build: it applies the Micronaut plugin, adds a number of dependencies, configures native image building, etc.

Then the "magic" is to use Gradle’s composition model for the variant aspects. For example, when we define the tcnative dimension with 2 variants on and off, we’re modeling the fact that there are 2 possible outcomes for this dimension. In practice, enabling tcnative is just a matter of adding a single dependency at runtime:

io.micronaut.testcase.tcnative.on.gradle.kts
dependencies {
    runtimeOnly("io.netty:netty-tcnative-boringssl-static::linux-x86_64")
}

The dimension which handles the version of Java (both to compile and run the application) makes use of Gradle’s toolchain support:

io.micronaut.testcase.java.17.gradle.kts
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

This can be done in a convention plugin which is named against the dimension variant name: io.micronaut.testcase.tcnative.on. In other words, the project with path :test-case:tcnative-off:epoll-off:json-jackson:micronaut-3.8:java-11 will have a "synthetic" build script which only consists of applying the following plugins:

plugins {
    id("io.micronaut.testcase")               (1)
    id("io.micronaut.testcase.tcnative.off")  (2)
    id("io.micronaut.testcase.epoll.off")     (3)
    id("io.micronaut.testcase.json.jackson")  (4)
    id("io.micronaut.testcase.micronaut.3.8") (5)
    id("io.micronaut.testcase.java.11")       (6)
}
1 Applies the common configuration
2 Configures tcnative off
3 Configures epoll off
4 Configures Jackson as the serialization framework
5 Configures Micronaut 3.8
6 Configures build for Java 11

Each of these plugins can be found in our build logic. As you can see when browsing the build logic directory, there is actually one small optimization: it is not necessary to create a variant script if there’s nothign to do. For example, in practice, tcnative off doesn’t need any extra configuration, so there’s no need to write a io.micronaut.testcase.tcnative.off plugin which would be empty in any case.

Variant specific code

The best case would have been that we only have to tweak the build process (for example to add dependencies, disable native image building, etc), but in some cases, we have to change the actual sources or resource files. Again, we leveraged Gradle’s flexibility to define custom conventions in our project layout. In a traditional Gradle (or Maven) project, the main sources are found in src/main/java. This is the case here, but we also support adding source directories based on the variants. For example in this project, some DTOs will make use of Java records on Java 17, but those are not available in Java 11, so we need to write 2 variants of the same classes: one with records, the other one with good old Java beans. This can be done by putting the Java 11 sources under src/main/variants/java-11/java, and their equivalent Java 17 sources under src/main/variants/java-17/java. This is actually generic: you can use any variant name in place of java-11: we could, for example, have a source directory for the epoll-on folder. The same behavior is available for resources (in src/main/variants/java-11/resources).

This provides very good flexibility while being totally understandable and conventional.

The settings plugin

So far, we explained how a user interacts with this build, for example by adding a dimension and a variant or adding specific sources, but we didn’t explain how the projects are actually generated. For this purpose, we have to explain that Gradle supports multiple types of plugins. The typical plugins, which we have used so far in this blog post, the io.micronaut.testcase.xxx plugins, are project plugins, because they apply on the Project of a Gradle build. There are other types of plugins, and the other one which we’re interested in here is the settings plugin. Unlike project plugins, these plugins are applied on the Settings object, that is to say thay they would be typically applied on the settings.gradle(.kts) file. This is what we have in this project:

settings.gradle.kts
// ...

plugins {
    id("io.micronaut.bench.variants")
}


include("load-generator-gatling")

configure<io.micronaut.bench.AppVariants> {
    combinations {
        dimension("tcnative") {
            variant("off")
            variant("on")
        }
        dimension("epoll") {
            variant("off")
            variant("on")
        }
        dimension("json") {
            variant("jackson")
            //variant("serde")
        }
        dimension("micronaut") {
            variant("3.8")
            //variant("4.0")
        }
        dimension("java") {
            //variant("11")
            variant("17")
        }
        exclude {
            // Combination of Micronaut 4 and Java 11 is invalid
            it.contains("micronaut-4.0") && it.contains("java-11")
        }
    }
}

The io.micronaut.bench.variants is another convention plugin defined in our build logic. It doesn’t do much, except for creating an extension, which is what lets us configure the variants:

import io.micronaut.bench.AppVariants

val variants = extensions.create<AppVariants>("benchmarkVariants", settings)

The logic actually happens within that AppVariants class, for which you can find the sources here. This class handles both the variants extension DSL and the logic to generate the projects.

The entry point is the combinations method which takes a configuration block. Each of the call to dimension registers a new dimension, which is itself configured via a variant configuration block, where each individual variant is declared. When we return from this call, we have built a model of dimension of variants, for which we need to compute the cartesian product.

We can check each of the entry that we have generated against the excludes, and if the combination is valid, we can use the Gradle APIs which are available in settings script to generate our synthetic projects.

For example:

val projectPath = ":test-case:${path.replace('/', ':')}"
settings.include(projectPath)

computes the project path (with colons) and includes it, which is equivalent to writing this manually in the settings.gradle file:

include(":test-case:tcnative-off:epoll-off:json-jackson:micronaut-3.8:java-11")
include(":test-case:tcnative-off:epoll-off:json-jackson:micronaut-3.8:java-17")
include(":test-case:tcnative-off:epoll-off:json-jackson:micronaut-4.0:java-17")

If we stopped here, then we would have defined projects, but Gradle would expect the sources and build scripts for these projects to be found in test-case/tcnative-off/epoll-off/json-jackson/micronaut-3.8/java-11. This isn’t the case for us, since all projects will share the same project directory (test-case-common). However, if we configure all the projects to use the same directory, then things could go wrong at build time, in particular because we use parallel builds: all the projects would write their outputs in the same build directory, but as we have seen, they may have different sources, different dependencies, etc. So we need to set both the project directory to the common directory, but also change the build directory to a per-project specific directory. This way we make sure to reuse the same sources without having to copy everything manually, but we also make sure that up-to-date checking, build caching and parallel builds work perfectly fine:

settings.project(projectPath).setProjectDir(File(settings.rootDir, "test-case-common"))
gradle.beforeProject {
    if (this.path == projectPath) {
        setBuildDir(File(projectDir, "build/${path}"))
    }
}

Note that we have to use the gradle.beforeProject API for this: it basically provides us with the naked Project instance of our synthetic projects, before its configuration phase is triggered.

The next step is to make sure that once the java plugin is applied on a project, we configure the additional source directories for each dimension. This is done via the withPlugin API which lets use react on the application of a plugin, and the SourceSet API:

project.plugins.withId("java") {
    project.extensions.findByType(JavaPluginExtension::class.java)?.let { java ->
        variantNames.forEach { variantName ->
            java.sourceSets.all {
                this.java.srcDir("src/$name/variants/$variantName/java")
                this.resources.srcDir("src/$name/variants/$variantName/resources")
            }
        }
    }
}

Last, we need to apply our convention plugins, the plugins which correspond to a specific combination variant, to our synthetic project:

gradle.afterProject {
    if (this.path == projectPath) {
        variantSpecs.forEach {
            val pluginId = "io.micronaut.testcase.${it.dimensionName}.${it.name}"
            val plugin = File(settings.settingsDir, "build-logic/src/main/kotlin/$pluginId.gradle.kts")
            if (plugin.exists()) {
                plugins.apply(pluginId)
            }
        }
    }
}

As you can see, for each variant, we basically compute the name of the plugin to apply, and if a corresponding file exists, we simply apply the plugin, that’s it!

It only takes around 100 lines of code to implement both the DSL and logic to generate all this, which is all the power Gradle gives us!

Limitations

Of course, there are limitations to this approach. While we could handle the Java version easily, we can’t, however, add a dimension we would have needed : GraalVM CE vs GraalVM EE. This is a limitation of Gradle’s toolchain support, which cannot make a difference between those 2 toolchains.

Another limitation is that this works well for a single project build, or a project like here where there’s a common application, a support library, but all modifications happen in a single project (the application). Supporting multi-project builds and variants per module would be possible in theory, but would add quite a lot of complexity.

It was also lucky that I could support both Micronaut 3 and Micronaut 4: in practice, the Gradle plugin for Micronaut 4 isn’t compatible with Micronaut 3, so I would have to either use Micronaut 3 or Micronaut 4. However, we can use the Micronaut 4 plugin with Micronaut 3, provided some small tweaks.

Last, there is one unknown to this, which is that building synthetic projects like that makes use of APIs which are stable in Gradle, but likely to be deprecated in the future (event based APIs).

Alternatives

Before going to the "final" solution, I have actually tried a few things (which could be spiked in a couple hours or so). In particular, the first thing I did was actually to use a single project, but configure additional artifacts (e.g jar and native binary) for each variant. While I could make it work, the implementation turned out to be more complicated, because you have to understand how each of the plugins work (Micronaut, GraalVM, the Shadow plugin) and create exotic tasks to make things work. Also this had a number of drawbacks:

  • impossible to build variants in parallel (at least without the experimental configuration cache)

  • configuring each of the variant specific build configuration (e.g adding dependencies) was more complicated. It was in particular only possible to add additional runtime dependencies. If something else was needed, for example compile time dependencies or additional resources, this wasn’t possible to do because a single main jar was produced.

Conclusion

In this blog post, we have seen how we can leverage Gradle’s flexibility to support what seemed to be a complicated use case: given a common codebase and some "small tweaks", generate a matrix of builds which are used to build different artifacts, in order to benchmark them.

The solution turned out to be quite simple to implement, and I hope pretty elegant, both in terms of user facing features (adding dimensions and configuring the build should be easy), maintenance (composition over inheritance makes it very simple to understand how things are combined) and implementation.

Many thanks to Jonas Konrad for the feature requests and for reviewing this blog post!

Petit voyage en électrique

20 January 2023

Tags: tourainetech peugeot electrique

Hier, je me déplaçais sur Tours pour la conférence Touraine Tech, où j’ai donné un talk sur Micronaut Test Resources. Je remercie encore l’organisation d’avoir accepté ce talk, qui, d’après les commentaires que j’ai reçu, a plutôt été bien reçu ! Mais ça n’est pas le sujet de ce billet : je souhaite simplement vous parler de mon expérience avec ma voiture électrique, que j’ai utilisé pour me rendre à la conférence.

Le déplacement

Tours, ça n’est pas si loin de chez moi, environ 200km. J’avais donc décidé de m’y rendre avec mon e-208, dont l’autonomie théorique, avec sa batterie de 50kW (disponible 46kW), est annoncée à 340km. J’ai fais l’acquisition de cette voiture il y a 2 ans, et j’en suis globalement très content : j’habite en zone rurale, nous n’avons pas de transports en commun, et cette voiture sert donc pour tous les trajets du quotidien. Je peux la recharger à la maison sans problème. Jusqu’ici, les trajets les plus longs que j’avais effectué étaient sans recharge : des allez-retours à Pornic, où j’ai de la famille, soit environ 160 km aller/retour, et ça se passait très bien, en particulier l’été.

Maintenant, entre l’autonomie théorique et la réalité, il y a un monde, en particulier en hiver. J’étais donc assez nerveux à l’idée de me retrouver "en rade" avant d’arriver sur Tours, et j’ai donc planifié mon déplacement avec l’application ChargeMap (j’ai une carte chez eux et l’application Peugeot est franchement pas top, impossible de planifier aussi bien).

Je voulais faire l’aller-retour dans la journée, ce qui impliquait de pouvoir recharger en arrivant sur Tours. Un des problèmes, c’est que les bornes de recharge "rapides" ne sont pas si nombreuses. Autre problème : il est impossible de savoir si une borne va être occupée lorsqu’on y arrivera. La 208 dispose d’une prise combo CCS qui accepte une charge à 100kW.

J’avais donc 2 choix:

  • m’arrêter à une charge rapide (50kW et +) avant de me rendre à la conférence

  • ou déposer ma voiture sur une borne lente à proximité de la conférence et revenir plus tard dans la journée pour libérer la borne

J’ai choisi la première option, parce que j’avais un doute que la borne soit occupée en arrivant, et que je doive donc faire 10 min de route de plus pour me rendre à la borne rapide et donc perdre du temps. Par ailleurs, ça n’est pas super pratique que de devoir quitter la conférence et marcher 1km (potentiellement sous la pluie) dans la journée.

En bref, j’ai planifié pour être tranquille. Voici les conditions du trajet:

  • départ 5h32, tout le trajet en mode éco

  • je suis parti avec une batterie chargée à 100% (je sais qu’il faut éviter, mais d’une, je n’allais pas risquer de devoir m’arrêter sur une borne lente en cours de trajet, je ne souhaitais pas me retrouver à moins de 10% de batterie à l’arrivée, trop stressant, et d’autre part, le logiciel Peugeot ne permet pas d’interrompre une charge lorsque la batterie atteint une certaine limite, par exemple 80% !)

  • j’ai choisi un itinéraire sans autoroute

  • j’ai roulé à 80km sur les départementales (y compris celles limitées à 90km/h, en Maine et Loire), entre 90 et 100km/h sur les nationales

  • je roule en conduite souple : pas d’accélérations brutales, utilisation du mode B pour freiner, etc…​

  • la température extérieure oscillait entre 0 et 3 degrés, chauffage réglé à 18

L’application ChargeMap dispose d’une fonctionnalité qui lui permet d’envoyer le trajet planifié sur Google Maps, que j’ai utilisé pour le guidage. Ça se passait très bien, jusqu’à ce que j’arrive après Saumur où je me rends compte que le GPS avait décidé de me faire prendre l’autoroute ! Problème, je n’avais clairement pas assez d’autonomie pour rouler à 130 km/h. Ne pouvant pas rouler à 90 km/h sur autoroute, trop dangereux, je suis donc monté à 110 km/h et autant dire que vu les conditions météo (froid), mon autonomie restante fondait comme neige au soleil. Je suis donc sorti un peu plus loin pour finir le trajet en passant par les bords de Loire, comme c’était initialement prévu.

Au final, je suis arrivé sur ma borne de recharge Allego, au Casino de La Riche, à 8h08 : 180km en 2h36. De mémoire (l’appli Peugeot récupère les trajets, mais pas la consommation, incroyable ce retard du logiciel par rapport à la concurrence !), ma consommation moyenne était de l’ordre de 16kWh/100km. Je me suis branché et j’ai chargé pendant 49 minutes pour atteindre 90% de batterie, soit 29kW, facture: 31,38€, pas franchement économique (1.082€ du kWh !). Je me suis arrêté à 90% parce que la recharge "ralentit" à mesure qu’on s’approche de la charge maximale : il m’aurait fallu rester encore une bonne demi-heure (voire plus) pour atteindre les 100%, et je souhaitais me rendre à la conférence.

Je suis donc arrivé sur Polytech’Tours à 9h12, soit 3h40, à comparer aux 2h25 si j’étais parti avec ma 407 diesel, qui ferait l’aller-retour sans aucun pb sans faire le plein (autonomie environ 950km…​).

Pour le retour, je savais donc que je serai très juste et qu’il faudrait probablement que je fasse un arrêt supplémentaire pour recharger (à cause des 10% de batterie en moins au départ). Je ne suis pas passé par l’autoroute au retour, et donc suivi les bords de Loire. Les conditions météo étaient similaires, mais avec plus de pluie. J’ai surveillé mon autonomie, et si au départ, j’avais une marge de 100km entre l’autonomie annoncée par la voiture (c’est à dire qu’en suivant ses indications, je serais à la maison avec 100km d’autonomie restante), au fur et à mesure du trajet, cette estimation a sensiblement baissé. Arrivé à Cholet (environ 40km de chez moi), il ne restait plus que 60 km de marge, alors que j’avais baissé la température dans l’habitacle à 16 degrés. Encore une fois, je roulais en mode éco, souple, pas de bouchons, rien. En clair, l’estimation d’autonomie, c’est du grand n’importe quoi et complètement irréaliste (à noter, qu’en été, c’est bien plus proche de la réalité).

Bref, j’avais aussi faim et n’étant pas très joueur, je me suis arrêté sur une borne rapide en chemin, à côté d’une pizzeria, au SIEML de l’Ecuyère. Comme je savais que quel que soit le temps de charge, j’aurais de quoi rentrer large, j’ai juste pris le temps de manger. Je récupère ma voiture, pas mal, 22,7kW de récupérés en 35 minutes de charge, pour 9.62€ : 3 fois moins cher que la recharge à Tours (mais à comparer aux 0.14€/kWh quand je charge à la maison…​).

Conclusion

L’expérience fut concluante : je sais que je peux faire ce genre de trajets, moyennant quelques concessions (heure d’arrivée tardive à cause de la recharge à l’arrivée, trajet sans autoroute, confort "limité", etc), mais c’est à peu près la distance maximale que je puisse faire sans que ça ne devienne trop pénible. En revanche, je reste très mitigé sur l’autonomie "réelle" : ici, j’étais plus proche des 200 km en faisant tout pour économiser. Même en conditions idéales, jamais, ô grand jamais, je n’atteindrais les 340 km (le mieux, c’est environ 300km). Le logiciel est aussi bien trop basique comparé à la concurrence (oui, Tesla) : l’application mobile manque de fonctionnalités de base (blocage de charge, récupération des consommations, …​) et le GPS pour planifier un trajet est franchement nul. L’estimation de l’autonomie restante est irréaliste et pire, on ne sait pas vraiment où on en est de charge: l’indicateur à la "jauge d’essence" n’est pas adapté à une batterie (dites moi le % restant, c’est plus parlant !). Enfin, un des gros soucis reste la recharge et les tarifs hallucinants qui sont pratiqués : il est pour ainsi dire impossible de savoir combien va vous coûter un trajet, puisqu’en fonction des conditions, vous allez devoir vous arrêter, ou non, et que les tarifs varient en fonction de la puissance de recharge, du fournisseur, etc.

Lorsqu’on part en thermique, on sait que le carburant coûte environ 1.9€/L, à 25% près : en électrique, oubliez. Vous pouvez faire du simple au quadruple. Est-ce à dire que je ne recommande pas l’électrique ? Pas du tout ! Déjà, je préfère 100 fois le confort de la conduite en électrique au thermique. La voiture est aussi super agréable à conduire et la puissance disponible immédiatement est un indéniable atout de l’électrique.

Mon alternative, sur ce trajet, aurait été de prendre ma voiture thermique, mais ça aurait été la solution de facilité. Et compte-tenu de l’urgence climatique, j’ai fais le choix de perdre un peu de confort, pour le bien de ma conscience :)

Un an d’astrophotographie

31 December 2022

Tags: astrophotography astronomy celestron

Note
An english version of this blog post is available here

Et voilà, 2022, c’est terminé ! Je poste régulièrement des images de l’univers que je prends depuis mon jardin, mais il est possible que vous en ayez raté quelques unes (je les postais sur Twitter, mais désormais, vous pouvez me suivre sur un compte astro dédié sur Mastodon), c’est donc l’opportunité de faire une rétrospective !

Prendre de belles photo du ciel demande un peu d’effort : bien entendu, il faut du matériel (télescope, caméras, …​), de bonnes conditions (météo, phase de la lune, …​) mais il faut aussi de l’expérience dans le traitement logiciel. Les photos que je prends ces temps-ci sont déja bien meilleures que celles que je prenais il y a quelques années. Néanmoins, j’y vois de nombreux défauts, du bruit, des problèmes de focus, etc. Il y a donc encore beaucoup de choses à améliorer.

Deux sessions Twitch

Cette année, j’ai fait deux sessions lives sur Twitch, où j’ai expliqué de mon point de vue les bases de l’astrophotographie. Il s’agit de deux sessions d’environ 90 minutes chacune, où je donne des informations sur le matériel, mon setup, mais aussi le traitement logiciel. Je les ai faites pour répondre à des questions qu’on me pose souvent, comme:

  • quel télescope choisir ?

  • combien de temps de pose faut-il ?

  • combien ça coûte ?

  • comment fais-tu pour prendre une photo ?

  • etc.

Si vous avez un peu de temps, voici les liens:

2022: Les photos

Je vais les lister par ordre chronologique.

Note
Les photos que je poste ici sont protégées par le droit d’auteur: vous ne pouvez ni les recopier, ni les revendre, sans ma permission.

De temps en temps, il peut s’écouler des semaines entre les photos, auquel cas il s’agit très certainement de problèmes météo : dès que j’en ai l’opportunité, vous pouvez être certains que je sors le matériel ! Cependant, il peut arriver qu’il s’agisse aussi d’un échec : paramètres de capture incorrects, mauvais focus, mauvaises poses de flats, …​ Dans ce cas, il n’y a guère d’autre choix que de mettre tout le travail à la poubelle…​

Pour chacune des photos que je vous propose ici, il s’agit d’une version basse résolution. Pour voir les versions hautes résolution, cliquez sur la photo, ce qui vous amènera vers sa version sur mon compte Astrobin. Vous aurez alors accès:

  • à tous les détails d’acquisition (quand, temps de pose, matériel utilisé, filtres, etc)

  • à une version "full" en haute résolution.

La plupart de mes photos font 62 megapixels. Parfois moins, parfois plus, mais il s’agit grossièrement de 7 fois plus de pixels que sur votre télé 4K ! Lorsque vous arriverez sur Astrobin, vous tomberez sur une version "annotée", mais basse résolution de la photo. Cliquez dessus et vous aurez alors accès à la version "full", qui est considérablement plus détaillée.

C’est parti !

Janvier

Nous sommes en période de fêtes, alors commençons par NGC 2264, aussi connu sous le nom de "l’arbre de Noël". Il s’agit d’un amas ouvert à environ 2300 années lumières, dans la constellation de la Licorne, qui se trouve accompagné d’une magnifique nébuleuse appelée la nébuleuse du cône.

2022 01 12 christmas tree cluster

Pendant l’hiver, les nuits sont relativement longues, ce qui donne souvent l’opportunité de travailler plusieurs sujets en une nuit. J’en ai donc profité pour capturer une autre cible, NGC 4565, la "galaxie de l’aiguille", un classique. Je n’ai pas pris suffisamment de données pour bien faire ressortir les couleurs, néamnoins le résultat est intéressant:

2022 01 12 ngc 4565

Février

A la fin de l’hiver, presque un mois plus tard, j’ai choisi une cible bien connue des astrophotographes : NGC 1893. Cette photo a été assez difficile à prendre et à traiter, à cause de la présence d’étoiles très brillantes dans le champ, ce qui, avec mon matériel, crée des reflets qu’il faut supprimer en traitement. Ce traitement pourrait même être améliorer pour réduire un peu plus les étoiles et faire ressortir la nébuleuse plus clairement.

2022 02 26 ic 410

Nous débutons alors la "saison des galaxies", qui dure tout le printemps. La prochaine image est un autre classique, le couple de galaxies M81 et M82. Je ne suis pas complètement satisfait du traitement de cette photo, notamment au niveau des couleurs, mais il s’agissait pour moi de faire ressortir ce qu’on appelle l’IFN, les Nébuleuses de flux intégré, qui ne sont ni plus ni moins que de très larges nuages de gaz "inerte" qui s’étendent sur de très grandes distances. C’est particulièrement visible dans cette photo.

2022 02 26 m81 m82

Mars

La prochaine photo est un autre classique du printemps, connu sur le nom de "la chaîne de Markarian". Bien que le grand public connaisse l’existence des galaxies et parfois des amas d’étoiles (notre soleil fait partie de la galaxie de la Voie Lactée), peu de personnes savent que les galaxies elle-mêmes s’organisent en "groupes locaux" puis sous forme de structures plus larges s’étendant sur des milliers d’années lumières. Cette photographie a le mérite de montrer ces groupes de façon évidente : sur une seule photographie, nous pouvons voir des dizaines de galaxies, parfois en intéraction directe (elles tournent les unes autour des autres et pourront fusionner), parfois simplement en arrière plan.

2022 03 23 Markarian chain

Mai

Le mois d’avril ne fut pas un bon mois, sans la moindre photo ! Alors quand Mai fut venu et le beau temps de retour, j’en ai profité et pris plein les mirettes !

La saison des galaxies n’est pas complètement terminée en Mai. J’ai choisi de capturer une galaxie dont la forme rappelle celle de la galaxie de l’aiguille: NGC 5746. Encore une fois sur cette photo, le traitement a été difficile à cause de la présence de cette étoile très brillante.

2022 05 09 ngc 5746

Continuons avec une des photos les plus spectaculaires de cette année. Elle n’est pas particulièrement nette et il y a des efforts à faire sur le traitement, mais il s’agissait là d’une occasion à ne pas manquer : la photographie d’un événement rare, une supernova, c’est à dire une étoile qui meurt dans une explosion phénoménale, en émettant une quantité de lumière si impressionnante qu’elle surpasse celle du reste de la galaxie : une supernova dans une galaxie va atteindre une luminosité apparente aussi forte que des étoiles de notre propre galaxie, alors qu’elle peut se situer à des centaines de milliers d’années lumières de nous !

La photo est aussi spectaculaire au sens où j’a utilisé une focale longue, pour "zoomer" au plus près, mais que malgré celà, de nombreuses autres galaxies sont visibles en arrière plan dans le champ ! Voici donc la supernova 2022hfs, dans la galaxie NGC 4647.

Sur la photo, au centre, nous avons la grande galaxie M60, et dessous, son compagnon NCG 4647. Dans cette dernière, on distingue en haut un point brillant: il s’agit de la supernova !

2022 05 07 supernova m60

Afin de changer des galaxies, j’ai aussi capturé en mai le magnifique amas globulaire nommé M5. Un amas globulaire est un ensemble de "vieilles" étoiles, regroupées dans un espace très compact. On trouve de nombreux amas globulaires au sein de notre galaxie.

2022 05 07 m5

J’ai une fascination pour les "nébuleuses sombres". Ce sont des nébuleuses de différents types, qui présentent des couleurs sombres pour différentes raisons: poussière interstellaire, qui bloque la lumière, ou plus simplement des régions du ciel dénuées des gas qui émettent typiquement dans le rouge. Faire ressortir ces nébuleuses sombre requiert en général beaucoup de temps de pose. En mai, ça reste possible, mais c’est difficile, et il faut même parfois cumuler les données de plusieurs nuits.

J’ai pris deux photos de telles nébuleuses en Mai. La première montre B347 et LDN 889, dans la constellation du Cygne. Il s’agit d’une nébuleuse en émission (elle émet sa propre lumière) qui montre une région plus sombre en son centre:

2022 05 28 ldn 914

La seconde est une magnifique structure complexe, mélange de gaz bloquants la lumière, de nébuleuses à réflexion (qui reflètent la lumière des étoiles) et à émission : IC 4603.

2022 05 29 ic 4603

En règle générale, j’évite la lune : je préfère photographier le ciel profond (galaxies, nébuleuses, …​), ce qui requiert un ciel sombre, sans lune et sans pollution lumineuse. Cependant, je m’accorde de temps en temps une pause, ce que j’ai fait le 10 mai, en capturant notre voisine:

2022 05 10 moon

Juin

En juin débute la saison des nébuleuses et en particulier des nébuleuses à émission (dans le rouge). Pendant quelques mois, nous allons pouvoir capturer beaucoup d’entre elles : ceci s’explique par la position de la voie lactée, qui en été se trouve au zénith au-dessus de nos têtes. Nous observons alors vers le centre de notre galaxie, où la densité d’étoiles et de nuages de gas est la plus forte.

Un des classique, c’est la nébuleuse de l’Aigle, que j’avais déja capturée. Je me suis donc lancé le défi de réaliser une mosaïque qui couvrirait une région assez large du ciel, de M16 à M24. La photo qui en résulte ne fait pas moins de 7360 pixels de large, pour 13200 de haut !

2022 05 30 M16 M17 M18 M24

La photo suivante est ma "favorite" en 2022. En comparaison à d’autres photos, elle ne m’a pas demandé beaucoup de temps de pose : seulement une heure, mais elle montre une des plus belles régions du ciel.

J’étais particulièrement envieux de prendre cette photo, parce qu’il est assez difficile de regrouper les conditions nécessaires là où j’habite : entre la météo et le fait que cette cible ne monte pas très haut dans le ciel, le timing était serré et je l’ai ratée plusieurs années de suite.

Trèves de bavardages, il s’agit donc des nébuleuses de la Trifide et du Lagon, qui, comme je l’ai dit, sont pour moi une des plus belles régions du ciel avec un mélange subtil de couleurs bleutées (nébuleuse en émission), rosée et rouge, avec de nombreux détails visibles dans les nébulosités.

2022 05 30 trifid lagoon nebulas v2

Lorce qu’on est astronome amateur, un des premiers objets du ciel profond que l’on essaie de voir l’été en Europe est la nébuleuse de la Lyre, M57. Il s’agit d’une nébuleuse planétaire (nommée ansi parce que les premiers observateurs les ont confondu avec des planètes) assez petite, mais très lumineuse, visible avec des instruments très modestes. La prendre en photo et faire ressortir des détails, en revanche, est une autre paire de manches. Pour cette photo, j’ai utilisé une technique inhabituelle, réservée d’habitude aux planètes : capturer de très nombreuses poses très courtes et "assembler" le tout. Voici le résultat:

2022 06 17 m57 ring nebula

Le 28 juin est une date spéciale, puisqu’il s’agit de mon anniversaire de mariage (20 ans en 2023 !). En 2022, le 28 juin me fit un beau cadeau avec de nombreuses photos.

La première est la nébuleuse du croissant, dans le Cygne: il s’agit d’une étoile qui a explosé et dont l’enveloppe gazeuse s’étend au milieu d’une gigantesque milieu gazeux rouge où naîtront de nouvelles étoiles.

2022 06 27 NGC 6888 crescent nebula

La 2ème est un autre classique des astrophotographes : la nébuleuse de la trompe d’éléphant (Tr-37). Je vous ai dis que j’aimais les nébuleuses sombres, cette photo montre de superbes structures sombres au sein d’une nébuleuse brillante, le contraste est saisissant !

2022 06 27 tr37 elephant trunk nebula

Un autre exemple d’une telle nébuleuse est NGC 7822, capturée la même nuit :

2022 06 28 Ced 214 NGC 7822

Enfin, pour conclure le mois de juin, il s’agit de vous montrer ce que l’on finit par voir après quelques milliers d’années, lorsqu’une supernova relativement proche explose. Le ciel d’été dispose d’un magnifique exemple dans la constellation du Cygne : la nébuleuse des "dentelles du Cygne", nommée ainsi en référence à ses magnifiques structures en filaments, de véritables "dentelles".

2022 06 27 veil nebula

Juillet

Le mois de juillet est la période idéale pour capturer notre voisine la plus proche, la galaxie d’Andromède. Légèrement plus grande que notre propre galaxie, la Voie Lactée, nos deux comparses finiront par fusionner dans quelques millions d’années. Andromède est visible à l’oeil nu sous un bon ciel sans pollution lumineuse. Sa taille apparente est même supérieuse à celle de la Lune !

2022 07 04 andromeda galaxy

Il y a de nombreuses comètes dans notre système solaire, et de temps en temps, une d’entre elles devient suffisamment brillante pour qu’on puisse la capturer. C’est arrivé pour moi au début du mois de Juillet, quand la comète C2017 K2 Panstarrs est devenue visible :

2022 07 06 c2017 k2 panstarrs

(pour une raison inconnue je n’ai pas posté cette photo sur Astrobin).

Ma cible suivante est un nouvel exemple de "nébuleuse sombre", cette fois ci sur une nébuleuse qui l’est de part des nuages de poussières interstellaires, qui empêchent la lumière des étoiles en fond de nous parvenir. Celle-ci est connue sous le nom de la nébuleuse de l’hippocampe. Mon champ est cependant suffisamment large pour montrer, dans la même photo, 2 autres objets très intéressants: l’amas ouvert NGC 6939, ainsi que la magnifique galaxie spirale NGC 6946, vue du dessus.

2022 07 06 seahorse nebula ngc 6946

Pour ma prochaine cible, j’ai choisi un objet plus difficile à prendre en photo : une nébuleuse à émission diffuse, connue sous le nom de Shapless 115 (du catalogue de nébuleuses du même nom). Prendre en photo de telles nébuleuses requiert l’utilisation de filtres en bande étroite, en particulier dans les endroits soumis à la pollution lumineuse, ce qui est mon cas (bien qu’elle soit bien plus raisonnable qu’à Nantes, 45 km plus au nord). Elle requiert aussi des temps de pose plus longs (ici, 4 heures) :

2022 07 31 Sh2 115 116 112

Le même jour, j’ai essayé de prendre en photo la "nébuleuse du sorcier", mais je fus assez déçu du résultat, que je n’ai donc pas posté sur Astrobin. Mais puisqu’il s’agit d’une rétrospective, je vous la pose ici, sachant que je réessairai certainement cette cible en 2023:

2022 07 26 ngc 7380 wizard nebula

Août

La prochaine photographie est une autre de mes favories en 2022, et une autre nébuleuse sombre. Il s’agit de la "nébuleuse du requin", j’espère que vous voyez pourquoi :

2022 08 01 ldn 1235 shark nebula

Vous ne voyez pas le requin ? Essayez avec cette version "sans étoiles" :

2022 08 01 ldn 1235 shark nebula starless

Pour la prochaine photographie, vous reconnaîtrez un visage familier : Andromède, une nouvelle fois ! Pourquoi la prendre en photo plusieurs fois ? Eh bien parce bien que ce soit une cible très grande et très lumineuse, il s’agit aussi d’une des plus difficile à traiter. Il faut donc essayer plusieurs techniques, plusieurs temps de pose, etc pour obtenir un bon résultat. Alors, quelle version préférez vous ?

2022 08 27 andromeda galaxy

Nous voici déja à la fin de l’été, mais il restait une photographie que je ne pouvais pas me permettre de rater : Messier 33, la galaxie du triangle. Il s’agit d’une autre galaxie de notre groupe local, avec Andromède, qui est absolument magnifique avec ses couleurs bleutées. Si vous zoomez sur la photos, vous y découvrirez de nombreuses régions rougeâtres ou rosées: il s’agit des mêmes types de nébuleuses que celles que j’ai montré précédement, où naissent de nombreuses nouvelles étoiles !

2022 08 28 m33 pinwheel

Septembre

Le mois de Septembre marque le début de la "saison des pluies", qui peut durer jusque février. Il reste encore quelques bonnes opportunités en septembre avant que cette saison ne commence.

Pour la première photo, nous avons NGC 7822, dont je ne suis pas particulièrement fier du traitement, avec de nombreux gradients encore visibles…​

2022 09 18 NGC 7822

La suivante est un autre "inmanquable", les nébuleuses de l’Amérique du Nord et du Pélican : cette photo est une mosaique de 2 images, n’hésitez donc pas à vous promener sur la version "full", qui révèle un nombre impressionnant de détails.

2022 09 18 North America and Pelican

Pour la suivante, vous retrouverez encore une fois une figure familière : Messier 33. Mais cette fois-ci, j’ai utilisé une autre focale, plus longue, permettant de zoomer sur la galaxie qui prenait alors l’intégralité du capteur, c’était juste parfait ! Là encore je vous invite à vous balader sur la version "full", qui montre nombre de détails…​

2022 09 20 Triangulum Galaxy M33

Novembre

Je vous avais dit qu’après Septembre, ça devenait compliqué…​ Entre fin septembre et fin novembre, pas la moindre photo ! Alors quand j’ai eu une petite fenêtre le 29, et peu de temps avant le retour de la pluie, j’ai choisi de partir sur une cible "facile" et très lumineuse, qui ne requiert donc pas beaucoup de temps de pose: la fameuse nébuleuse d’Orion !

2022 11 29 orion m42

La nuit suivante, j’ai eu une nouvelle chance, où j’ai choisi de cibler la très photogénique NGC 1333, une région du ciel qui montre dans un même champ des nébuleuses sombres, mais aussi de lumineuses nébuleuses à réflection, presque blanches, et une région rougeâtre où naissent de nombreuses étoiles formant un magnifique contraste avec la région sombre alentour !

2022 11 30 ngc 1333

Decembre

Pour terminer l’année, une photo qui n’a que quelques jours. J’ai dù attendre le lendemain de Noël pour pouvoir ressortir le matériel (et profiter de mes vacances). Les conditions étaient loin d’être parfaites : de nombreux passages nuageux, un ciel pas super transparent, et une humidité qui finit par tomber en brume. Cependant, cette cible me fait de l’oeil depuis de nombreuses années, et là encore, il s’agit d’une cible qu’on ne peut pas capturer facilement de part sa hauteur, très basse sur l’horizon. La "fenêtre de capture" est donc très courte, et j’aurais aimé disposer de plus de temps de pose pour faire ressortir des couleurs du "casque de Thor" que voici:

2022 12 26 ngc 2359 Thor s Helmet

Et nous y voilà ! J’aurais bien sûr aimé en faire plus. Mais entre les pleines lunes, les conditions météo et n’oublions pas mon travail, c’est toujours un peu compliqué. Il y a de très nombreux objets que je souhaite prendre en photo, et que j’ai soit raté, soit pas eu l’occasion ou le temps de faire lorsqu’ils étaient visibles. Qu’à celà ne tienne, c’est aussi pour ça que l’astrophotographie, c’est une passion d’une vie !


Older posts are available in the archive.