Skip to content

Memoize device template to improve rule evaluation performance#429

Merged
ianrumac merged 7 commits into
developfrom
claude/devicetemplate-memoization-cjke25
Jul 7, 2026
Merged

Memoize device template to improve rule evaluation performance#429
ianrumac merged 7 commits into
developfrom
claude/devicetemplate-memoization-cjke25

Conversation

@ianrumac

@ianrumac ianrumac commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Changes in this pull request

  • Memoizes the device template used for audience-filter evaluation and paywall templating. The template is now rebuilt only when one of its mutable inputs (identity, entitlements, subscription status, etc.) changes, rather than on every rule evaluation.

  • Separates volatile and stable template fields:

    • Stable fields (identity, device info, entitlements) are cached and reused
    • Volatile fields (time-derived values like utcDateTime, daysSinceLastPaywallView) are recomputed and overlaid on cache hits
    • This ensures time-sensitive fields stay current while avoiding expensive rebuilds
  • Optimizes property access:

    • Converts frequently-accessed properties (appVersion, vendorId, deviceId, kotlinVersion, etc.) to lazy-initialized values
    • Caches PowerManager system service lookup
    • Consolidates date formatting logic into a single volatileTemplateFields() function
  • Refactors template building:

    • Extracts buildDeviceTemplate() method for clarity
    • Introduces templateFingerprint() to detect when cache invalidation is needed
    • Adds VolatileTemplateFields data class to manage time-derived values
    • Uses AtomicReference for thread-safe cache management
  • Removes duplicate code:

    • Eliminates redundant daysSinceLastPaywallView and minutesSinceLastPaywallView properties
    • Consolidates date/time formatting properties into a single computation
  • Adds comprehensive test coverage (DeviceHelperMemoizationTest):

    • Verifies template is memoized when inputs don't change
    • Confirms cache invalidation on identity, entitlements, and platform wrapper changes
    • Validates time-derived fields are overlaid fresh on cache hits
    • Tests null handling for optional time-since fields

Checklist

  • All unit tests pass (added DeviceHelperMemoizationTest with 5 test cases)
  • Existing DeviceTemplateTest updated to verify toDictionary() compatibility
  • I added an entry to the CHANGELOG.md for this enhancement
  • Code follows existing patterns (lazy initialization, coroutine usage, error handling)

Performance Impact

This change significantly reduces CPU and memory overhead during rule evaluation:

  • Template building involves ~55 field accesses and JSON serialization
  • With memoization, this is skipped on cache hits (the common case)
  • Time-derived fields are computed efficiently in a single pass rather than individually
  • Fingerprint comparison is O(1) in-memory operation vs. full template rebuild

Building the device template assembled ~55 fields and ran a full
encodeToString/decodeFromString round trip on every call — per rule, per
evaluation, plus twice concurrently during config fetch. Each build also
repeated system IPCs (Settings.Secure ANDROID_ID, PackageManager lookups)
and allocated six SimpleDateFormats.

- Cache the serialized template keyed by a fingerprint of every mutable
  input (identity, entitlements, subscription status, active products,
  interface style, platform wrapper, locale/timezone, storage counters,
  storefront). Any change to those inputs rebuilds the template; enrichment
  and experimental properties stay outside the cache and are applied fresh.
- Overlay time-derived fields (utc/local date-time strings, days/minutes
  since install and last paywall view) on every read instead of keying the
  cache on them, deriving all of them from a single timestamp.
- Memoize process-constant fields: vendorId/deviceId (Settings.Secure),
  app version/build/install date (reuse the constructor-cached
  PackageInfo), padded versions, capabilities JSON, kotlinVersion.
- Replace the JSON string round trip in DeviceTemplate.toDictionary with
  encodeToJsonElement and a direct JsonObject-to-map conversion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukxof6pYoqDdcFMvwCtb23
@ianrumac ianrumac force-pushed the claude/devicetemplate-memoization-cjke25 branch from 7dc686a to 7134a20 Compare July 7, 2026 14:51
claude and others added 6 commits July 7, 2026 15:12
8615b5d wrapped message parsing in runCatching so unknown messages
don't crash the batch, but left the original unguarded add in place.
Every message was added twice, and an unknown message still threw and
failed the whole parse. Keep only the guarded add and log a warning
when a message can't be parsed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukxof6pYoqDdcFMvwCtb23
Follow-ups to get CI green after the develop changes this branch sits on:

- parseWrappedPaywallMessages: skip messages with unknown event names
  (logging a warning) instead of catching every parse error. Newer
  paywalls can send events an older SDK doesn't know about without
  breaking the batch, while a malformed payload for a known event still
  fails the whole parse, as parsePageView_missingRequiredField_fails
  expects. Update parse_unknown_event_returns_failure to the new
  semantics and have it prove a known message alongside the unknown one
  still parses.

- PaywallViewDismissTest: pass forceCleanup = true to beforeOnDestroy/
  destroyed. Since 4a4ad72 those are no-ops on non-finishing stops;
  production reaches teardown via forceCleanup = isFinishing, and these
  tests simulate a dismissal that finishes the Activity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukxof6pYoqDdcFMvwCtb23
Feature PRs land in develop, so preload regressions were only caught at
release time when a PR into main ran the benchmark. Trigger it on PRs
into develop too; merges already kept baselines fresh via push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ukxof6pYoqDdcFMvwCtb23
@ianrumac ianrumac merged commit 55e9716 into develop Jul 7, 2026
@ianrumac ianrumac deleted the claude/devicetemplate-memoization-cjke25 branch July 8, 2026 09:20
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.

2 participants