Skip to content

RUM-16056 Add @HotMethod annotation and HotMethodIllegalCall detekt rule#3558

Open
satween wants to merge 2 commits into
developfrom
tvaleev/feature/dd-sdk-android-hotmethod-RUM-16056
Open

RUM-16056 Add @HotMethod annotation and HotMethodIllegalCall detekt rule#3558
satween wants to merge 2 commits into
developfrom
tvaleev/feature/dd-sdk-android-hotmethod-RUM-16056

Conversation

@satween

@satween satween commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Introduces a @HotMethod source annotation to mark frame/touch hot-path methods and a HotMethodIllegalCall detekt rule that forbids heap allocations and O(N) collection operations inside them. Adds a dedicated detekt_expensive_methods.yml config to activate the rule. Annotates existing hot-path methods in RUM, Session Replay, and Compose; pre-allocates objects where needed to satisfy the rule.

Motivation

Methods called on every frame or touch event (JankStats callbacks, dispatchTouchEvent, onDraw) are a common source of GC pressure and UI jank. Making them machine-verifiable via a detekt rule prevents regressions from creeping back in during normal development.

Additional Notes

The @Suppress("HotMethodIllegalCall") annotations on two sites (one SlowFrameRecord allocation and one TargetNode allocation) document intentional exceptions: both allocations are bounded by actual jank/tap events rather than frame rate, so suppression is intentional and annotated with an explanation comment.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

Ref: RUM-16056

Introduces a source-level @HotMethod annotation to mark frame/touch hot-path
methods, a new detekt rule (HotMethodIllegalCall) that forbids heap allocations
and O(N) collection operations inside them, and a dedicated detekt config
(detekt_expensive_methods.yml). Annotates existing hot-path methods in RUM,
Session Replay, and Compose and pre-allocates objects to suppress legitimate
findings.

Ref: RUM-16056
@satween satween marked this pull request as ready for review June 18, 2026 16:32
@satween satween requested review from a team as code owners June 18, 2026 16:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 72ec5c04a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@datadog-datadog-prod-us1-2

This comment has been minimized.

Comment thread detekt_expensive_methods.yml
@sbarrio sbarrio requested review from hamorillo and jonathanmos June 19, 2026 12:59
…s to annotation

Adds `val exclude: Array<String> = []` to `@HotMethod` so callers can silence
specific checks per call site instead of suppressing the whole rule. The six
check-name constants (CHECK_CONSTRUCTOR, CHECK_LAMBDA, etc.) now live as a
public companion object on `HotMethod` in `dd-sdk-android-internal`; the detekt
rule and its tests import them from there. Adds `dd-sdk-android-internal` as a
`compileOnly`/`testImplementation` dependency to `tools/detekt` — safe because
the constants are `const val` strings inlined at compile time.

Ref: RUM-16056

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ff26b422c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

val expr = excludeArg.getArgumentExpression() ?: return emptySet()
return (expr as? KtCollectionLiteralExpression)
?.getInnerExpressions()
?.filterIsInstance<KtStringTemplateExpression>()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve constant exclude entries

When a hot method uses the provided constants, e.g. @HotMethod(..., exclude = [HotMethod.CHECK_CONSTRUCTOR]), the array element is a qualified constant reference rather than a string template, so this filter drops it and the exclude set remains empty. That makes the documented per-site suppressions unreliable unless authors use raw string literals, causing false positives for legitimate exclusions.

Useful? React with 👍 / 👎.

private fun KtLambdaExpression.isInlinedLambda(): Boolean {
val containingCall = findContainingCallExpression() ?: return false
val calleeName = containingCall.calleeExpression?.text
val isAllowlisted = calleeName != null && calleeName in allowedInlineFunctionsSet

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require resolved inline status before suppressing lambdas

Because detekt_expensive_methods.yml allowlists common names such as forEach, map, and find, this name-only check treats any call with one of those names as inline. In a @HotMethod, a Java Stream.forEach { ... } or a custom non-inline forEach/map therefore escapes the lambda-allocation report even though the lambda object is still allocated; prefer the resolved isInline result when it is available or qualify the allowlist.

Useful? React with 👍 / 👎.

return
}

if (matchesForbiddenEntry(containerFqName, functionName, forbiddenCallsSet) &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Distinguish safe no-argument list overloads

This collection check matches only the receiver and function name, so entries like List.first, List.last, List.any, and List.count also report the no-argument overloads (list.first(), list.count(), etc.). Those overloads are O(1) and non-allocating for List, so enabling this rule with maxIssues: 0 will block safe hot-path code unless the match includes the resolved parameter list or the config avoids names with safe overloads.

Useful? React with 👍 / 👎.

- 'kotlin.text.buildString'
forbiddenCalls:
# Linear search
- 'kotlin.collections.List.find'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cover mutable collection receivers

With the current List-only entries, a hot method using a MutableList receiver (for example listeners.forEach { ... } or items.map { ... }) is not matched because the resolver uses the receiver's static type (kotlin.collections.MutableList), which does not equal or suffix-match kotlin.collections.List.*. This leaves common mutable hot-path collections outside the new guardrail; add the mutable/collection receiver variants or match supertypes.

Useful? React with 👍 / 👎.


// Constructor calls (PascalCase) are detected via PSI — the type-resolved path treats some
// Java constructors differently and may not emit functionName=="constructor" reliably.
override fun visitCallExpression(expression: KtCallExpression) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Detect iterator allocations in for-loops

This implementation only inspects call expressions and a few literal PSI nodes, but Kotlin for (item in list) loops are KtForExpressions rather than source call expressions. A future @HotMethod can therefore iterate a List directly and allocate an Iterator on every frame without any finding, even though the rule is meant to enforce the index-loop pattern used in the newly annotated frame callbacks.

Useful? React with 👍 / 👎.

val hotAnnotation = function.annotationEntries.find {
it.shortName?.asString() == HOT_METHOD_ANNOTATION
}
functionDepthStack.addLast(hotAnnotation?.extractExcludedChecks())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle anonymous function literals

Anonymous function literals such as val f = fun() { ... } are visited as KtNamedFunctions without a @HotMethod annotation, so this line pushes null and leaves the hot-method context instead of reporting the function-object allocation. Because fun() {} is an alternative to a lambda literal, hot paths can still allocate per invocation while bypassing the new lambda check.

Useful? React with 👍 / 👎.

# Both lists use `ContainerFQN.method` format (or `SimpleClass.method` as a suffix match).
# For top-level factory functions the container is the package (e.g. kotlin.collections).
# emptyList/emptySet/emptyMap are intentionally excluded — they return cached singletons.
forbiddenFactoryFunctions:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add array factories to forbidden factories

The factory list omits lower-case array factories like arrayOf, arrayOfNulls, and primitive *ArrayOf calls, so @HotMethod fun f() { val xs = arrayOf(...) } allocates a new array without being caught by either the PascalCase constructor heuristic or this config. Add the kotlin.* array factory entries if the rule is intended to block heap allocations in hot paths.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants