diff options
Diffstat (limited to 'adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java')
-rw-r--r-- | adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java | 331 |
1 files changed, 331 insertions, 0 deletions
diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java b/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java new file mode 100644 index 0000000000..ba4fd5c57e --- /dev/null +++ b/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.adservices.service.adselection; + +import android.adservices.adselection.AdSelectionConfig; +import android.adservices.common.AdTechIdentifier; +import android.adservices.exceptions.AdServicesException; +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.Pair; + +import com.android.adservices.LogUtil; +import com.android.adservices.data.adselection.AdSelectionEntryDao; +import com.android.adservices.data.adselection.DBAdSelection; +import com.android.adservices.data.customaudience.CustomAudienceDao; +import com.android.adservices.data.customaudience.DBCustomAudience; +import com.android.adservices.service.Flags; +import com.android.adservices.service.common.AdServicesHttpsClient; +import com.android.adservices.service.common.AppImportanceFilter; +import com.android.adservices.service.common.FledgeAllowListsFilter; +import com.android.adservices.service.common.FledgeAuthorizationFilter; +import com.android.adservices.service.common.Throttler; +import com.android.adservices.service.consent.ConsentManager; +import com.android.adservices.service.devapi.DevContext; +import com.android.adservices.service.stats.AdServicesLogger; +import com.android.adservices.service.stats.ApiServiceLatencyCalculator; +import com.android.internal.annotations.VisibleForTesting; + +import com.google.common.base.Function; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.time.Clock; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** Orchestrate on-device ad selection. */ +public class OnDeviceAdSelectionRunner extends AdSelectionRunner { + @NonNull protected final AdsScoreGenerator mAdsScoreGenerator; + @NonNull protected final AdServicesHttpsClient mAdServicesHttpsClient; + @NonNull protected final AdBidGenerator mAdBidGenerator; + + public OnDeviceAdSelectionRunner( + @NonNull final Context context, + @NonNull final CustomAudienceDao customAudienceDao, + @NonNull final AdSelectionEntryDao adSelectionEntryDao, + @NonNull final AdServicesHttpsClient adServicesHttpsClient, + @NonNull final ExecutorService lightweightExecutorService, + @NonNull final ExecutorService backgroundExecutorService, + @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, + @NonNull final ConsentManager consentManager, + @NonNull final AdServicesLogger adServicesLogger, + @NonNull final DevContext devContext, + @NonNull AppImportanceFilter appImportanceFilter, + @NonNull final Flags flags, + @NonNull final Supplier<Throttler> throttlerSupplier, + int callerUid, + @NonNull final FledgeAuthorizationFilter fledgeAuthorizationFilter, + @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter, + @NonNull final ApiServiceLatencyCalculator apiServiceLatencyCalculator) { + super( + context, + customAudienceDao, + adSelectionEntryDao, + lightweightExecutorService, + backgroundExecutorService, + scheduledExecutor, + consentManager, + adServicesLogger, + appImportanceFilter, + flags, + throttlerSupplier, + callerUid, + fledgeAuthorizationFilter, + fledgeAllowListsFilter, + apiServiceLatencyCalculator); + + Objects.requireNonNull(adServicesHttpsClient); + + mAdServicesHttpsClient = adServicesHttpsClient; + mAdBidGenerator = + new AdBidGeneratorImpl( + context, + mAdServicesHttpsClient, + mLightweightExecutorService, + mBackgroundExecutorService, + mScheduledExecutor, + devContext, + mCustomAudienceDao, + flags); + mAdsScoreGenerator = + new AdsScoreGeneratorImpl( + new AdSelectionScriptEngine( + mContext, + () -> flags.getEnforceIsolateMaxHeapSize(), + () -> flags.getIsolateMaxHeapSizeBytes()), + mLightweightExecutorService, + mBackgroundExecutorService, + mScheduledExecutor, + mAdServicesHttpsClient, + devContext, + mAdSelectionEntryDao, + flags); + } + + @VisibleForTesting + OnDeviceAdSelectionRunner( + @NonNull final Context context, + @NonNull final CustomAudienceDao customAudienceDao, + @NonNull final AdSelectionEntryDao adSelectionEntryDao, + @NonNull final AdServicesHttpsClient adServicesHttpsClient, + @NonNull final ExecutorService lightweightExecutorService, + @NonNull final ExecutorService backgroundExecutorService, + @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, + @NonNull final ConsentManager consentManager, + @NonNull final AdsScoreGenerator adsScoreGenerator, + @NonNull final AdBidGenerator adBidGenerator, + @NonNull final AdSelectionIdGenerator adSelectionIdGenerator, + @NonNull Clock clock, + @NonNull final AdServicesLogger adServicesLogger, + @NonNull AppImportanceFilter appImportanceFilter, + @NonNull final Flags flags, + @NonNull final Supplier<Throttler> throttlerSupplier, + int callerUid, + @NonNull final FledgeAuthorizationFilter fledgeAuthorizationFilter, + @NonNull final FledgeAllowListsFilter fledgeAllowListsFilter, + @NonNull final ApiServiceLatencyCalculator apiServiceLatencyCalculator) { + super( + context, + customAudienceDao, + adSelectionEntryDao, + lightweightExecutorService, + backgroundExecutorService, + scheduledExecutor, + consentManager, + adSelectionIdGenerator, + clock, + adServicesLogger, + appImportanceFilter, + flags, + throttlerSupplier, + callerUid, + fledgeAuthorizationFilter, + fledgeAllowListsFilter, + apiServiceLatencyCalculator); + + Objects.requireNonNull(adsScoreGenerator); + Objects.requireNonNull(adServicesHttpsClient); + Objects.requireNonNull(adBidGenerator); + + mAdsScoreGenerator = adsScoreGenerator; + mAdServicesHttpsClient = adServicesHttpsClient; + mAdBidGenerator = adBidGenerator; + } + + /** + * Orchestrate on device ad selection. + * + * @param adSelectionConfig Set of data from Sellers and Buyers needed for Ad Auction and + * Selection + */ + public ListenableFuture<AdSelectionOrchestrationResult> orchestrateAdSelection( + @NonNull final AdSelectionConfig adSelectionConfig, + @NonNull final String callerPackageName, + ListenableFuture<List<DBCustomAudience>> buyerCustomAudience) { + AsyncFunction<List<DBCustomAudience>, List<AdBiddingOutcome>> bidAds = + buyerCAs -> { + return runAdBidding(buyerCAs, adSelectionConfig); + }; + + ListenableFuture<List<AdBiddingOutcome>> biddingOutcome = + Futures.transformAsync(buyerCustomAudience, bidAds, mLightweightExecutorService); + + AsyncFunction<List<AdBiddingOutcome>, List<AdScoringOutcome>> mapBidsToScores = + bids -> { + return runAdScoring(bids, adSelectionConfig); + }; + + ListenableFuture<List<AdScoringOutcome>> scoredAds = + Futures.transformAsync( + biddingOutcome, mapBidsToScores, mLightweightExecutorService); + + Function<List<AdScoringOutcome>, AdScoringOutcome> reduceScoresToWinner = + scores -> { + return getWinningOutcome(scores); + }; + + ListenableFuture<AdScoringOutcome> winningOutcome = + Futures.transform(scoredAds, reduceScoresToWinner, mLightweightExecutorService); + + Function<AdScoringOutcome, AdSelectionOrchestrationResult> mapWinnerToDBResult = + scoringWinner -> { + return createAdSelectionResult(scoringWinner); + }; + + ListenableFuture<AdSelectionOrchestrationResult> dbAdSelectionBuilder = + Futures.transform(winningOutcome, mapWinnerToDBResult, mLightweightExecutorService); + + return dbAdSelectionBuilder; + } + + private ListenableFuture<List<AdBiddingOutcome>> runAdBidding( + @NonNull final List<DBCustomAudience> customAudiences, + @NonNull final AdSelectionConfig adSelectionConfig) { + if (customAudiences.isEmpty()) { + LogUtil.w("Cannot invoke bidding on empty list of CAs"); + return Futures.immediateFailedFuture(new Throwable("No CAs found for selection")); + } + + Map<AdTechIdentifier, List<DBCustomAudience>> buyerToCustomAudienceMap = + mapBuyerToCustomAudience(customAudiences); + PerBuyerBiddingRunner buyerBidRunner = + new PerBuyerBiddingRunner( + mAdBidGenerator, mScheduledExecutor, mBackgroundExecutorService); + + LogUtil.v("Invoking bidding for #%d buyers", buyerToCustomAudienceMap.size()); + return Futures.successfulAsList( + buyerToCustomAudienceMap.entrySet().parallelStream() + .map( + entry -> { + return buyerBidRunner.runBidding( + entry.getKey(), + entry.getValue(), + mFlags.getAdSelectionBiddingTimeoutPerBuyerMs(), + adSelectionConfig); + }) + .flatMap(List::stream) + .collect(Collectors.toList())); + } + + @SuppressLint("DefaultLocale") + private ListenableFuture<List<AdScoringOutcome>> runAdScoring( + @NonNull final List<AdBiddingOutcome> adBiddingOutcomes, + @NonNull final AdSelectionConfig adSelectionConfig) + throws AdServicesException { + LogUtil.v("Got %d total bidding outcomes", adBiddingOutcomes.size()); + List<AdBiddingOutcome> validBiddingOutcomes = + adBiddingOutcomes.stream().filter(Objects::nonNull).collect(Collectors.toList()); + LogUtil.v("Got %d valid bidding outcomes", validBiddingOutcomes.size()); + + if (validBiddingOutcomes.isEmpty()) { + LogUtil.w("Received empty list of successful Bidding outcomes"); + throw new IllegalStateException(ERROR_NO_VALID_BIDS_FOR_SCORING); + } + return mAdsScoreGenerator.runAdScoring(validBiddingOutcomes, adSelectionConfig); + } + + private AdScoringOutcome getWinningOutcome( + @NonNull List<AdScoringOutcome> overallAdScoringOutcome) { + LogUtil.v("Scoring completed, generating winning outcome"); + return overallAdScoringOutcome.stream() + .filter(a -> a.getAdWithScore().getScore() > 0) + .max( + (a, b) -> + Double.compare( + a.getAdWithScore().getScore(), + b.getAdWithScore().getScore())) + .orElseThrow(() -> new IllegalStateException(ERROR_NO_WINNING_AD_FOUND)); + } + + /** + * This method populates an Ad Selection result ready to be persisted in DB, with all the fields + * except adSelectionId and creation time, which should be created as close as possible to + * persistence logic + * + * @param scoringWinner Winning Ad for overall Ad Selection + * @return A {@link Pair} with a Builder for {@link DBAdSelection} populated with necessary data + * and a string containing the JS with the decision logic from this buyer. + */ + @VisibleForTesting + AdSelectionOrchestrationResult createAdSelectionResult( + @NonNull AdScoringOutcome scoringWinner) { + DBAdSelection.Builder dbAdSelectionBuilder = new DBAdSelection.Builder(); + LogUtil.v("Creating Ad Selection result from scoring winner"); + dbAdSelectionBuilder + .setWinningAdBid(scoringWinner.getAdWithScore().getAdWithBid().getBid()) + .setCustomAudienceSignals( + scoringWinner.getCustomAudienceBiddingInfo().getCustomAudienceSignals()) + .setWinningAdRenderUri( + scoringWinner.getAdWithScore().getAdWithBid().getAdData().getRenderUri()) + .setBiddingLogicUri( + scoringWinner.getCustomAudienceBiddingInfo().getBiddingLogicUri()) + .setContextualSignals("{}"); + // TODO(b/230569187): get the contextualSignal securely = "invoking app name" + return new AdSelectionOrchestrationResult( + dbAdSelectionBuilder, + scoringWinner.getCustomAudienceBiddingInfo().getBuyerDecisionLogicJs()); + } + + private Map<AdTechIdentifier, List<DBCustomAudience>> mapBuyerToCustomAudience( + final List<DBCustomAudience> customAudienceList) { + Map<AdTechIdentifier, List<DBCustomAudience>> buyerToCustomAudienceMap = new HashMap<>(); + + for (DBCustomAudience customAudience : customAudienceList) { + buyerToCustomAudienceMap + .computeIfAbsent(customAudience.getBuyer(), k -> new ArrayList<>()) + .add(customAudience); + } + LogUtil.v("Created mapping for #%d buyers", buyerToCustomAudienceMap.size()); + return buyerToCustomAudienceMap; + } + + private int getParallelBiddingCount() { + int parallelBiddingCountConfigValue = mFlags.getAdSelectionConcurrentBiddingCount(); + int numberOfAvailableProcessors = Runtime.getRuntime().availableProcessors(); + return Math.min(parallelBiddingCountConfigValue, numberOfAvailableProcessors); + } +} |