Improve RealContentScopeScripts.getScript() - #9335
Merged
anikiki merged 4 commits intoJul 30, 2026
Merged
Conversation
…nfig handling and caching.
Builds on the plugin config cache by removing work that still ran on every navigation, and freezes the flag-off path so disabling optimizeContentScopeInjection is a true rollback. Optimized path: - Rebuild the content scope JSON only when the plugin config or the unprotected temporary exceptions actually changed, reusing the already serialized exceptions instead of re-serializing and re-comparing the whole string on every call. - Read the feature flag once per call instead of at each use site, so a flag flip can no longer mix the two paths within a single call. - Cache the messaging parameters and the version/platform constants, and embed the messaging parameters when building the preferences JSON so the template assembly no longer substitutes a placeholder. - Call Toggle.getCohort() once per toggle rather than twice; it can enrol to self-heal a stale cohort, so the second call was not a free read. - Never latch the startup stabilisation check on an empty plugin config, which could otherwise freeze an empty feature config for the session if two early navigations both ran before the feature repos finished loading. - Invalidate the cached plugin config when the legacy path runs, so a flag flip mid-session cannot resurrect config that was dropped while it was disabled. Legacy path is unchanged end to end: every call runs with optimized = false through the original helpers, throwaway Moshi instances, cohort filter/map and chained-replace assembly. It is deleted together with the flag once the optimized path is rolled out. Adds tests for the flag flip, the empty-config latch, the exceptions-only rebuild after settling, the download-does-not-invalidate contract, and the per-flag call counts that pin the legacy freeze. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
Author
This stack of pull requests is managed by Graphite. Learn more about stacking. |
anikiki
marked this pull request as ready for review
July 29, 2026 13:20
The cache could latch onto default protections for the rest of the process.
Feature repos load their persisted config into memory asynchronously with no
completion signal, so the cache used a stabilisation latch: keep sweeping the
plugins until the assembled config stops changing, then trust the dirty flag.
The latch's empty-config guard does not reach the case it was written for. A
plugin whose repo has not loaded does not report an empty config, it reports the
entity's default JSON wrapped in the feature key ("elementHiding":{}), so the
assembled string is non-empty and stable during the whole cold-start window.
Two navigations inside that window settle the latch on defaults, and nothing
reloads it: onPrivacyConfigPersisted() fires once per process from
LocalPrivacyConfigObserver, racing the repo loads rather than following them, so
recovery needs a remote config download. Offline, never. The symptom is element
hiding, fingerprint protection and cookie protection silently absent, with no
crash and no pixel.
Assemble the config on every call again, via the single getPluginParameters()
pass that also builds the preferences, and keep cachedPluginConfig purely as a
comparison baseline. Every input is now read per call, so nothing can go stale,
and the savings that motivated the cache are unaffected: an unchanged config
still skips re-serializing the unprotected temporary exceptions, rebuilding the
content scope JSON and reassembling the template. The preferences sweep already
ran unconditionally, so this replaces two separate sweeps with one.
Removes the PrivacyConfigCallbackPlugin binding, which existed only to
invalidate the cache, and the flag-flip write that kept the two paths in step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…path Two changes, both confined to the optimized path so that disabling optimizeContentScopeInjection stays a true rollback. Plugin parameter assembly no longer reassigns Strings. The original builds the config and preferences by `+=` per plugin, which copies the whole accumulator each time: quadratic in the size of the assembled config, across ~22 plugins, on the main thread on every navigation. getOptimizedPluginParameters accumulates into StringBuilders instead. The original is renamed getLegacyPluginParameters and kept byte-for-byte, matching the existing getLegacyScript / getOptimizedScript pairing, with each twin naming the other so the duplication is visible at both declarations. Duplicated deliberately rather than shared, to leave the flag-off path untouched. Experiment cohorts no longer enter runBlocking when there is nothing to read. getActiveExperiments() returns only enabled toggles, so the list is empty whenever no contentScope experiment is running, and the result is then the constant "currentCohorts":[]. That case now returns before runBlocking, which also removes the main-thread runBlocking from the common path. EMPTY_COHORTS is built from EMPTY_JSON_LIST so it cannot drift from what Moshi emits for an empty list. Both are output-preserving, and the existing whenOptimizeInjectionEnabledThenOutputIsByteIdenticalToFallbackPath test proves it: it normalises the random messaging secrets and compares the two paths in full, with an empty experiment list. Its fixture never stubbed preferences(), so the preferences accumulator was compared while empty; it now has two preferences contributors as well as two config ones. Verified by mutation: changing the optimized separator to ';' and changing EMPTY_COHORTS both fail it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7 tasks
CrisBarreiro
approved these changes
Jul 30, 2026
anikiki
deleted the
feature/ana/improve_realcontentscopescripts_getscript
branch
July 30, 2026 16:24
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Task/Issue URL: https://app.asana.com/1/137249556945/project/1200581511062568/task/1216821003646786?focus=true
Tech Design URL (if applicable):
API Proposals URL(s) (if applicable):
Description
Reduces the per-navigation cost of
getScript()without changing its output, behind theoptimizeContentScopeInjectionkill-switch (DefaultFeatureValue.INTERNAL). Every input is still read on every call, so nothing can go stale — the savings are all downstream, in skipping work whose inputs didn't change and doing the rest more cheaply.Optimized path:
config()andpreferences()accumulate intoStringBuilders instead of reassigning aStringper plugin, which copied the whole accumulator each time — quadratic in the assembled config size, across ~22 plugins, on the main thread.runBlockingis skipped entirely when no contentScope experiment is active —getActiveExperiments()returns only enabled toggles, so the result is then a constant.Toggle.getCohort()is called once per toggle instead of twice; it can enrol to self-heal a stale cohort, so the second call wasn't a free read.An earlier revision cached the assembled plugin config and invalidated it on privacy-config persistence.
That was removed: a feature repo that hasn't finished its async load reports its default JSON rather than an
empty string, so the startup stabilisation latch could settle on defaults and serve them for the rest of the
process.
Deliberately unchanged: allow list, unprotected temporary, plugin
config()andpreferences(), cohorts anddesktop mode are evaluated on every call, so config-driven and user-facing settings (GPC, forced zoom,
allowlisting) apply immediately.
Rollback: with the flag off, every call runs
optimized = falseend to end through the original helpers —throwaway Moshi instances, the original quadratic parameter assembly, the original cohort filter/map, and the
chained-replace assembly. The legacy path is byte-and-behaviour identical to before and is deleted with the
flag once rollout completes.
whenOptimizeInjectionEnabledThenOutputIsByteIdenticalToFallbackPathcomparesthe two paths in full, and two further tests pin the freeze via the per-flag
getCohort()andappBuildConfigread counts.Steps to test this PR
flag ON (default on internal)
globalPrivacyControlValuetakes effect (sec-gpc: 1is notsent when GPC is off).
Rollback is clean
NO UI changes
Note
Medium Risk
Changes how often privacy-config-driven script JSON is rebuilt and when plugin config updates apply (persist vs startup latch), which could affect protection behavior if invalidation or settling logic is wrong; legacy path preserved behind a kill-switch.
Overview
Adds an optimized
getScript()path behindoptimizeContentScopeInjection, while keeping the prior logic asgetLegacyScript()for rollback.Caching & invalidation: Assembled plugin
config()is cached and only rebuilt whenonPrivacyConfigPersisted()setspluginConfigNeedsRebuild, or during startup until the config stabilizes (empty configs never latch).RealContentScopeScriptsnow implementsPrivacyConfigCallbackPlugin;onPrivacyConfigDownloaded()is intentionally a no-op.Per-call work: Allow list, plugin
preferences(), desktop mode, and cohorts still update every navigation. Content-scope JSON is rebuilt only when cached plugin config or unprotected-temporary exceptions change. Messaging secrets, version/platform strings, reusable Moshi adapters, and single-pass template assembly are reused on the optimized path; cohort reads go from twogetCohort()calls to one per toggle.Flag behavior: Mid-session flag flips route entirely through one path; legacy sets
pluginConfigNeedsRebuildso switching back to optimized does not resurrect stale plugin config.Reviewed by Cursor Bugbot for commit 1d54e28. Bugbot is set up for automated code reviews on this repo. Configure here.