RUM-16056 Add @HotMethod annotation and HotMethodIllegalCall detekt rule#3558
RUM-16056 Add @HotMethod annotation and HotMethodIllegalCall detekt rule#3558satween wants to merge 2 commits into
@HotMethod annotation and HotMethodIllegalCall detekt rule#3558Conversation
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
There was a problem hiding this comment.
💡 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".
This comment has been minimized.
This comment has been minimized.
…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
There was a problem hiding this comment.
💡 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>() |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) && |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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 👍 / 👎.
What does this PR do?
Introduces a
@HotMethodsource annotation to mark frame/touch hot-path methods and aHotMethodIllegalCalldetekt rule that forbids heap allocations and O(N) collection operations inside them. Adds a dedicateddetekt_expensive_methods.ymlconfig 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 (oneSlowFrameRecordallocation and oneTargetNodeallocation) 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)
Ref: RUM-16056