Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metric overhaul #1504

Merged
merged 28 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
64d23c6
Metric overhaul
rorbech Sep 6, 2023
902e8f7
Support gradle configuration caching
rorbech Sep 8, 2023
8f869f4
Apply suggestions from code review
rorbech Sep 11, 2023
544a3c4
Updates according to review comments
rorbech Sep 11, 2023
50a79be
Fix json formatting
rorbech Sep 12, 2023
da915cc
Merge remote-tracking branch 'origin/cr/metrics-overhaul' into cr/met…
rorbech Sep 12, 2023
4b7ee93
Merge branch 'main' into cr/metrics-overhaul
rorbech Sep 19, 2023
410bbd4
Read core version through platform agnostic APIs
rorbech Sep 19, 2023
2c861cf
Update documentation of dumping options
rorbech Sep 19, 2023
73501df
Fix windows build.
cmelchior Sep 21, 2023
7dcf74c
Enable collection of metrics in CI builds
rorbech Sep 22, 2023
ddad510
Update builder id string when failing
rorbech Sep 22, 2023
27da2a0
Fix cocoapods reference in collected metrics
rorbech Sep 22, 2023
0cb3003
Bump minimum supported Gradle version
rorbech Sep 27, 2023
cb866a6
Change info message prefix
rorbech Sep 27, 2023
5351adb
Gradle version differentiation and error handling
rorbech Oct 9, 2023
18aaa89
Add gradle version test projects
rorbech Oct 10, 2023
cb61636
Clean up gradle compatibility tests
rorbech Oct 10, 2023
6b51f79
Run Gradle8 tests with JDK 17
rorbech Oct 11, 2023
471a9bb
Fix gradle8 build issue
rorbech Oct 11, 2023
cd59445
Fix jenkins environment for gradle 8 build
rorbech Oct 11, 2023
34f2882
Merge branch 'main' into cr/metrics-overhaul
rorbech Oct 11, 2023
01e10a1
Upgrade minimum supported Kotlin version
rorbech Oct 13, 2023
9f9a301
Add gradle integration project with current version
rorbech Oct 17, 2023
d4b65d6
Revert minimum supported Gradle version
rorbech Oct 17, 2023
aff72ae
Revert project settings
rorbech Oct 17, 2023
a22da77
Merge branch 'main' into cr/metrics-overhaul
rorbech Nov 14, 2023
a3598de
Bump Kotlin version in integration tests to fit new minimum version
rorbech Nov 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions packages/gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/
import kotlin.text.toBoolean
import java.io.ByteArrayOutputStream

plugins {
kotlin("jvm")
Expand Down Expand Up @@ -93,22 +94,37 @@ sourceSets {
java.srcDir(versionDirectory)
}
}
tasks.create("pluginVersion") {
val outputDir = file(versionDirectory)

// Task to fetch core version from dependency.list from core submodule
tasks.create("coreVersion", Exec::class.java) {
workingDir = project.file(listOf("..", "external", "core").joinToString(File.separator))
commandLine = listOf("grep", "^VERSION", "dependencies.list")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if grep is available on Windows? It would probably be safer to use the Kotlin API to read the file content and find it that way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn it. You and your windows machine 😅

Copy link
Contributor Author

@rorbech rorbech Sep 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated it to read the file through Kotlin API on top of Gradles file content provider API so now it should be fully platform agnostic and appropriately regenerate if external/core/dependencies.list is updated.

standardOutput = ByteArrayOutputStream()
doLast {
extra["output"] = standardOutput.toString().trim().split("=")[1]
}
}

// Task to generate gradle plugin runtime constants for SDK and core versions
tasks.create("versionConstants") {
val outputDir = file(versionDirectory)
inputs.property("version", project.version)
outputs.dir(outputDir)

dependsOn(tasks["coreVersion"])
doLast {
val versionFile = file("$outputDir/io/realm/kotlin/gradle/version.kt")
val coreVersion = (tasks["coreVersion"] as Exec).extra["output"]
versionFile.parentFile.mkdirs()
versionFile.writeText(
"""
// Generated file. Do not edit!
package io.realm.kotlin.gradle
internal const val PLUGIN_VERSION = "${project.version}"
internal const val CORE_VERSION = "${coreVersion}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that we often point to commits that are not tagged releases, this might not be 100% accurate. But I assume that appending the submodule commit sha might be a bit tricky, so maybe this fine?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not tricky ... on the client side. But it will probably clutter the remote view. I guess we should rather just strive to use tagged core-releases 😉 For discussion see https://mongodb.slack.com/archives/C04M17MCY0H/p1693920525143069

""".trimIndent()
)
}
}
tasks.getByName("compileKotlin").dependsOn("pluginVersion")

tasks.getByName("compileKotlin").dependsOn("versionConstants")
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,118 @@

package io.realm.kotlin.gradle

import com.android.build.gradle.BaseExtension
import io.realm.kotlin.gradle.analytics.AnalyticsService
import io.realm.kotlin.gradle.analytics.BuilderId
import io.realm.kotlin.gradle.analytics.ComputerId
import io.realm.kotlin.gradle.analytics.HOST_ARCH
import io.realm.kotlin.gradle.analytics.HOST_OS
import io.realm.kotlin.gradle.analytics.ProjectConfiguration
import io.realm.kotlin.gradle.analytics.TargetInfo
import io.realm.kotlin.gradle.analytics.hexStringify
import io.realm.kotlin.gradle.analytics.sha256Hash
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.provider.Provider
import org.gradle.api.services.BuildServiceSpec
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin
import org.jetbrains.kotlin.gradle.plugin.SubpluginArtifact
import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension
import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin.Companion.COCOAPODS_EXTENSION_NAME
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinSharedNativeCompilation
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaCompilation
import org.jetbrains.kotlin.konan.target.Architecture
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File

class RealmCompilerSubplugin : KotlinCompilerPluginSupportPlugin {

private lateinit var anonymizedBundleId: String
private var analyticsServiceProvider: Provider<AnalyticsService>? = null

companion object {
// TODO LATER Consider embedding these from the build.gradle's pluginVersion task just
// TODO LATER Consider embedding these from the build.gradle's versionConstants task just
// as with the version. But leave it for now as they should be quite stable.
// Modules has to match ${project.group}:${project.name} to make composite build work
const val groupId = "io.realm.kotlin"
const val artifactId = "plugin-compiler"
const val version = PLUGIN_VERSION
const val coreVersion = CORE_VERSION

// The id used for passing compiler options from command line
const val compilerPluginId = "io.realm.kotlin"

// Must match io.realm.kotlin.compiler.bundleIdKey
const val bundleIdKey = "bundleId"

// Must match io.realm.kotlin.compiler.
const val featureListPathKey = "featureListPath"
}

@Suppress("NestedBlockDepth")
override fun apply(target: Project) {
super.apply(target)

// We build the anonymized bundle id here and pass it to the compiler plugin to ensure
// that the metrics and sync connection parameters are aligned.
val bundleId = target.rootProject.name + ":" + target.name
anonymizedBundleId = hexStringify(sha256Hash(bundleId.toByteArray()))

val disableAnalytics: Boolean = target.gradle.startParameter.isOffline || "true".equals(
System.getenv()["REALM_DISABLE_ANALYTICS"],
ignoreCase = true
)
if (!disableAnalytics) {
// Identify if project is using sync by inspecting dependencies.
// We cannot use resolved configurations here as this code is called in
// afterEvaluate, and resolving it prevents other plugins from modifying
// them. E.g the KMP plugin will crash if we resolve the configurations
// in `afterEvaluate`. This means we can only see dependencies directly set,
// and not their transitive dependencies. This should be fine as we only
// want to track builds directly using Realm.
var usesSync = false
outer@
for (conf in target.configurations) {
for (dependency in conf.dependencies) {
if (dependency.group == "io.realm.kotlin" && dependency.name == "library-sync") {
// In Java we can detect Sync through a Gradle configuration closure.
// In Kotlin, this choice is currently determined by which dependency
// people include
usesSync = true
break@outer
}
}
}

val userId = target.providers.of(ComputerId::class.java) {}.get()
val builderId = target.providers.of(BuilderId::class.java) {}.get()
val verbose = "true".equals(System.getenv()["REALM_PRINT_ANALYTICS"], ignoreCase = true)

analyticsServiceProvider = target.gradle.sharedServices.registerIfAbsent(
"Realm Analytics",
AnalyticsService::class.java
) { spec: BuildServiceSpec<ProjectConfiguration> ->
spec.parameters.run {
this.appId.set(anonymizedBundleId)
this.userId.set(userId)
this.builderId.set(builderId)
this.hostOsType.set(HOST_OS.serializedName)
this.hostOsVersion.set(System.getProperty("os.version"))
this.hostCpuArch.set(HOST_ARCH)
this.usesSync.set(usesSync)
this.verbose.set(verbose)
}
}
}
}

override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean {
Expand All @@ -56,10 +149,138 @@ class RealmCompilerSubplugin : KotlinCompilerPluginSupportPlugin {

override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> {
val project = kotlinCompilation.target.project
val realmPlugin = project.plugins.getPlugin("io.realm.kotlin") as RealmPlugin
val anonymizedBundleId = realmPlugin.anonymizedBundleId

// Compiler plugin options
val options = mutableListOf(
SubpluginOption(key = bundleIdKey, anonymizedBundleId),
)
// Only bother collecting info if the analytics service is registered
analyticsServiceProvider?.let { provider ->
// Enable feature collection in compiler plugin by setting a path for the feature list
// file and pass it to the compiler plugin as a compiler plugin option
val featureListPath = listOf(
project.buildDir.path,
"outputs",
"realm-features",
kotlinCompilation.defaultSourceSet.name
).joinToString(File.separator)
options.add(SubpluginOption(key = featureListPathKey, featureListPath))

// Gather target specific information
val targetInfo: TargetInfo? = gatherTargetInfo(kotlinCompilation)
// If we have something to submit register it for submission after the compilation has
// gathered the feature list information
targetInfo?.let {
kotlinCompilation.compileTaskProvider.get().doLast {
nhachicha marked this conversation as resolved.
Show resolved Hide resolved
analyticsServiceProvider!!.get().submit(targetInfo)
}
}
}
return project.provider {
listOf(SubpluginOption(key = bundleIdKey, anonymizedBundleId))
options
}
}
}

@Suppress("ComplexMethod", "NestedBlockDepth")
private fun gatherTargetInfo(kotlinCompilation: KotlinCompilation<*>): TargetInfo? {
val project = kotlinCompilation.target.project
return when (kotlinCompilation) {
// We don't send metrics for common targets but still collect features as the
// target specific features is a union of common and target specific features
is KotlinCommonCompilation,
is KotlinSharedNativeCompilation ->
null

is KotlinJvmAndroidCompilation -> {
val androidExtension =
project.extensions.findByName("android") as BaseExtension?
val defaultConfig = androidExtension?.defaultConfig
val minSDK = defaultConfig?.minSdkVersion?.apiString
val targetSDK = defaultConfig?.targetSdkVersion?.apiString
val targetCpuArch: String =
defaultConfig?.ndk?.abiFilters?.singleOrNull()?.let { androidArch(it) }
?: "Universal"
TargetInfo("Android", targetCpuArch, targetSDK, minSDK)
}

is KotlinJvmCompilation -> {
val jvmTarget = kotlinCompilation.kotlinOptions.jvmTarget
TargetInfo("JVM", "Universal", jvmTarget, jvmTarget)
}

is KotlinNativeCompilation -> {
// We currently only support Darwin targets, so assume that we can pull minSdk
// from the given deploymentTarget. Non-CocoaPod Xcode project have this in its
// pdxproj-file as <X>OS_DEPLOYMENT_TARGET, but assuming that most people use
// CocoaPods as it is the default. Reevaluate if we see too many missing values.
val kotlinExtension: KotlinMultiplatformExtension =
project.extensions.getByType(KotlinMultiplatformExtension::class.java)
val cocoapodsExtension =
(kotlinExtension as ExtensionAware).extensions.getByName(
COCOAPODS_EXTENSION_NAME
) as CocoapodsExtension?
val minSdk = cocoapodsExtension?.let { cocoapods ->
when (kotlinCompilation.konanTarget.family) {
Family.OSX -> cocoapods.osx.deploymentTarget
Family.IOS -> cocoapods.ios.deploymentTarget
Family.TVOS -> cocoapods.tvos.deploymentTarget
Family.WATCHOS -> cocoapods.watchos.deploymentTarget
Family.LINUX,
Family.MINGW,
Family.ANDROID,
Family.WASM,
Family.ZEPHYR -> null // Not supported yet
}
}
TargetInfo(
nativeTarget(kotlinCompilation.konanTarget),
nativeArch(kotlinCompilation.konanTarget),
null,
minSdk
)
}
// Not supported yet so don't try to gather target information
is KotlinJsCompilation,
is KotlinWithJavaCompilation<*, *> -> null

else -> {
null
}
}
}

// Helper method to ensure that we align target type string for native builds
fun nativeTarget(target: KonanTarget) = when (target.family) {
Family.OSX -> "macOS"
Family.IOS -> "iOS"
Family.TVOS -> "tvOS"
Family.WATCHOS -> "watchOS"
Family.LINUX -> "Linux"
Family.MINGW -> "MinGW"
Family.ANDROID -> "Android(native)"
Family.WASM -> "Wasm"
Family.ZEPHYR -> "Zephyr"
else -> "Unknown[${target.family}]"
}

// Helper method to ensure that we align architecture strings for Kotlin native builds
fun nativeArch(target: KonanTarget) = when (target.architecture) {
Architecture.X64 -> io.realm.kotlin.gradle.analytics.Architecture.X64.serializedName
Architecture.X86 -> io.realm.kotlin.gradle.analytics.Architecture.X86.serializedName
Architecture.ARM64 -> io.realm.kotlin.gradle.analytics.Architecture.ARM64.serializedName
Architecture.ARM32 -> io.realm.kotlin.gradle.analytics.Architecture.ARM.serializedName
Architecture.MIPS32 -> "Mips"
Architecture.MIPSEL32 -> "MipsEL32"
Architecture.WASM32 -> "Wasm"
else -> "Unknown[${target.architecture}]"
}

// Helper method to ensure that we align architecture strings for Android platforms
fun androidArch(target: String): String = when (target) {
"armeabi-v7a" -> io.realm.kotlin.gradle.analytics.Architecture.ARM.serializedName
"arm64-v8a" -> io.realm.kotlin.gradle.analytics.Architecture.ARM64.serializedName
"x86" -> io.realm.kotlin.gradle.analytics.Architecture.X86.serializedName
"x86_64" -> io.realm.kotlin.gradle.analytics.Architecture.X64.serializedName
else -> "Unknown[$target]"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,20 @@

package io.realm.kotlin.gradle

import io.realm.kotlin.gradle.analytics.AnalyticsService
import io.realm.kotlin.gradle.analytics.hexStringify
import io.realm.kotlin.gradle.analytics.sha256Hash
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.DependencySubstitutions
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import org.gradle.api.provider.Provider
import org.gradle.build.event.BuildEventsListenerRegistry
import javax.inject.Inject

@Suppress("unused")
open class RealmPlugin : Plugin<Project> {

private val logger: Logger = Logging.getLogger("realm-plugin")

internal lateinit var anonymizedBundleId: String

@Inject
public open fun getBuildEventsRegistry(): BuildEventsListenerRegistry { TODO("Should have been replaced by Gradle.") }

override fun apply(project: Project) {
project.pluginManager.apply(RealmCompilerSubplugin::class.java)

// We build the anonymized bundle id here and pass it to the compiler plugin to ensure
// that the metrics and sync connection parameters are aligned.
val bundleId = project.rootProject.name + ":" + project.name
anonymizedBundleId = hexStringify(sha256Hash(bundleId.toByteArray()))

// Run analytics as a Build Service to support Gradle Configuration Cache
val serviceProvider: Provider<AnalyticsService> = project.gradle.sharedServices.registerIfAbsent(
"realm-analytics",
AnalyticsService::class.java
) { /* Do nothing */ }
getBuildEventsRegistry().onTaskCompletion(serviceProvider)

project.configurations.all { conf: Configuration ->
// Ensure that android unit tests uses the Realm JVM variant rather than Android.
// This is a bit britle. See https://github.com/realm/realm-kotlin/issues/1404 for
Expand All @@ -71,28 +47,5 @@ open class RealmPlugin : Plugin<Project> {
}
}
}

// Stand alone Android projects have not initialized kotlin plugin when applying this, so
// postpone dependency injection till after evaluation.
project.afterEvaluate {
// TODO AUTO-SETUP To ease configuration we could/should inject dependencies to our
// library, but await better insight into when/what to inject and supply appropriate
// opt-out options through our own extension?
// Dependencies should probably be added by source set and not by target, as
// kotlin.sourceSets.getByName("commonMain").dependencies (or "main" for Android), but

// Create the analytics during configuration because it needs access to the project
// in order to gather project relevant information in afterEvaluate. Currently
// there doesn't seem a way to get this information during the Execution Phase.
@Suppress("SwallowedException", "TooGenericExceptionCaught")
try {
val analyticsService: AnalyticsService = serviceProvider.get()
analyticsService.collectAnalyticsData(it)
} catch (ex: Exception) {
// Work-around for https://github.com/gradle/gradle/issues/18821
// Since this only happens in multi-module projects, this should be fine as
// the build will still be registered by the first module that starts the service.
}
}
}
}
Loading