1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.adservices.test.scenario.adservices.fledge;
18 
19 import android.adservices.adselection.AdSelectionConfig;
20 import android.adservices.adselection.AdSelectionOutcome;
21 import android.adservices.clients.adselection.AdSelectionClient;
22 import android.adservices.clients.customaudience.AdvertisingCustomAudienceClient;
23 import android.adservices.common.AdData;
24 import android.adservices.common.AdSelectionSignals;
25 import android.adservices.common.AdTechIdentifier;
26 import android.adservices.customaudience.CustomAudience;
27 import android.adservices.customaudience.TrustedBiddingData;
28 import android.adservices.test.scenario.adservices.utils.SelectAdsFlagRule;
29 import android.adservices.test.scenario.adservices.utils.StaticAdTechServerUtils;
30 import android.content.Context;
31 import android.net.Uri;
32 import android.platform.test.rule.CleanPackageRule;
33 import android.platform.test.rule.KillAppsRule;
34 import android.platform.test.scenario.annotation.Scenario;
35 import android.util.Log;
36 
37 import androidx.test.core.app.ApplicationProvider;
38 
39 import com.android.adservices.common.AdServicesFlagsSetterRule;
40 import com.android.adservices.common.AdservicesTestHelper;
41 import com.android.compatibility.common.util.ShellUtils;
42 import com.android.modules.utils.build.SdkLevel;
43 
44 import com.google.common.base.Stopwatch;
45 import com.google.common.base.Ticker;
46 import com.google.common.collect.ImmutableList;
47 import com.google.common.collect.ImmutableMap;
48 import com.google.common.io.CharStreams;
49 
50 import org.json.JSONArray;
51 import org.json.JSONObject;
52 import org.junit.Assert;
53 import org.junit.BeforeClass;
54 import org.junit.Rule;
55 import org.junit.rules.RuleChain;
56 import org.junit.runner.RunWith;
57 import org.junit.runners.JUnit4;
58 
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.io.InputStreamReader;
62 import java.nio.charset.StandardCharsets;
63 import java.time.Instant;
64 import java.time.temporal.ChronoUnit;
65 import java.util.ArrayList;
66 import java.util.List;
67 import java.util.concurrent.Executor;
68 import java.util.concurrent.Executors;
69 import java.util.concurrent.TimeUnit;
70 
71 @Scenario
72 @RunWith(JUnit4.class)
73 public class AbstractSelectAdsLatencyTest {
74     protected static final String TAG = "SelectAds";
75 
76     private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool();
77 
78     private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
79     protected static final int API_RESPONSE_TIMEOUT_SECONDS = 100;
80     protected static final AdSelectionClient AD_SELECTION_CLIENT =
81             new AdSelectionClient.Builder()
82                     .setContext(CONTEXT)
83                     .setExecutor(CALLBACK_EXECUTOR)
84                     .build();
85     protected static final AdvertisingCustomAudienceClient CUSTOM_AUDIENCE_CLIENT =
86             new AdvertisingCustomAudienceClient.Builder()
87                     .setContext(CONTEXT)
88                     .setExecutor(CALLBACK_EXECUTOR)
89                     .build();
90     protected final Ticker mTicker =
91             new Ticker() {
92                 public long read() {
93                     return android.os.SystemClock.elapsedRealtimeNanos();
94                 }
95             };
96     protected static List<CustomAudience> sCustomAudiences;
97 
98     // Per-test method rules, run in the given order.
99     @Rule
100     public RuleChain rules =
101             RuleChain.outerRule(
102                             // CleanPackageRule should not execute after each test method because
103                             // there's a chance it interferes with ShowmapSnapshotListener snapshot
104                             // at the end of the test, impacting collection of memory metrics for
105                             // AdServices process.
106                             new CleanPackageRule(
107                                     AdservicesTestHelper.getAdServicesPackageName(CONTEXT),
108                                     /* clearOnStarting = */ true,
109                                     /* clearOnFinished = */ false))
110                     .around(
111                             new KillAppsRule(
112                                     AdservicesTestHelper.getAdServicesPackageName(CONTEXT)))
113                     .around(new SelectAdsFlagRule());
114 
115     @Rule
116     public final AdServicesFlagsSetterRule flags =
117             AdServicesFlagsSetterRule.forAllApisEnabledTests().setCompatModeFlags();
118 
119     @BeforeClass
setupBeforeClass()120     public static void setupBeforeClass() {
121         StaticAdTechServerUtils.warmupServers();
122         sCustomAudiences = new ArrayList<>();
123     }
124 
runSelectAds( String customAudienceJson, String adSelectionConfig, String testClassName, String testName)125     protected void runSelectAds(
126             String customAudienceJson,
127             String adSelectionConfig,
128             String testClassName,
129             String testName)
130             throws Exception {
131         // TODO(b/266194876): Clean up CA db entries before starting a test run. Cleaning up CAs
132         // would ensure we run select ads only the CAs added by the test.
133         sCustomAudiences.addAll(readCustomAudiences(customAudienceJson));
134         joinCustomAudiences(sCustomAudiences);
135         AdSelectionConfig config = readAdSelectionConfig(adSelectionConfig);
136 
137         Stopwatch timer = Stopwatch.createStarted(mTicker);
138         AdSelectionOutcome outcome =
139                 AD_SELECTION_CLIENT
140                         .selectAds(config)
141                         .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
142         timer.stop();
143 
144         Log.i(TAG, generateLogLabel(testClassName, testName, timer.elapsed(TimeUnit.MILLISECONDS)));
145         Assert.assertFalse(outcome.getRenderUri().toString().isEmpty());
146     }
147 
disableJsCache()148     protected void disableJsCache() throws Exception {
149         ShellUtils.runShellCommand(
150                 "device_config put adservices fledge_http_cache_enable_js_caching false");
151         // TODO(b/266194876): Clean up cache db entries when cache is disabled. Cleaning up cache
152         // would ensure we are not occuping memory unnecessarily.
153     }
154 
enableJsCache()155     protected void enableJsCache() throws Exception {
156         ShellUtils.runShellCommand(
157                 "device_config put adservices fledge_http_cache_enable_js_caching true");
158     }
159 
warmupSingleBuyerProcess()160     protected void warmupSingleBuyerProcess() throws Exception {
161         joinCustomAudiences(readCustomAudiences("CustomAudiencesOneBuyerOneCAOneAd.json"));
162         AD_SELECTION_CLIENT
163                 .selectAds(readAdSelectionConfig("AdSelectionConfigOneBuyerOneCAOneAd.json"))
164                 .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
165     }
166 
warmupFiveBuyersProcess()167     protected void warmupFiveBuyersProcess() throws Exception {
168         joinCustomAudiences(readCustomAudiences("CustomAudiencesFiveBuyersOneCAFiveAds.json"));
169         AD_SELECTION_CLIENT
170                 .selectAds(readAdSelectionConfig("AdSelectionConfigFiveBuyersOneCAFiveAds.json"))
171                 .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
172     }
173 
readCustomAudiences(String fileName)174     private ImmutableList<CustomAudience> readCustomAudiences(String fileName) throws Exception {
175         ImmutableList.Builder<CustomAudience> customAudienceBuilder = ImmutableList.builder();
176         InputStream is = ApplicationProvider.getApplicationContext().getAssets().open(fileName);
177         JSONArray customAudiencesJson = new JSONArray(readString(is));
178         is.close();
179 
180         for (int i = 0; i < customAudiencesJson.length(); i++) {
181             JSONObject caJson = customAudiencesJson.getJSONObject(i);
182             JSONObject trustedBiddingDataJson = caJson.getJSONObject("trustedBiddingData");
183             JSONArray trustedBiddingKeysJson =
184                     trustedBiddingDataJson.getJSONArray("trustedBiddingKeys");
185             JSONArray adsJson = caJson.getJSONArray("ads");
186 
187             ImmutableList.Builder<String> biddingKeys = ImmutableList.builder();
188             for (int index = 0; index < trustedBiddingKeysJson.length(); index++) {
189                 biddingKeys.add(trustedBiddingKeysJson.getString(index));
190             }
191 
192             ImmutableList.Builder<AdData> adDatas = ImmutableList.builder();
193             for (int index = 0; index < adsJson.length(); index++) {
194                 JSONObject adJson = adsJson.getJSONObject(index);
195                 adDatas.add(
196                         new AdData.Builder()
197                                 .setRenderUri(Uri.parse(adJson.getString("render_uri")))
198                                 .setMetadata(adJson.getString("metadata"))
199                                 .build());
200             }
201 
202             customAudienceBuilder.add(
203                     new CustomAudience.Builder()
204                             .setBuyer(AdTechIdentifier.fromString(caJson.getString("buyer")))
205                             .setName(caJson.getString("name"))
206                             .setActivationTime(Instant.now())
207                             .setExpirationTime(Instant.now().plus(90000, ChronoUnit.SECONDS))
208                             .setDailyUpdateUri(Uri.parse(caJson.getString("dailyUpdateUri")))
209                             .setUserBiddingSignals(
210                                     AdSelectionSignals.fromString(
211                                             caJson.getString("userBiddingSignals")))
212                             .setTrustedBiddingData(
213                                     new TrustedBiddingData.Builder()
214                                             .setTrustedBiddingKeys(biddingKeys.build())
215                                             .setTrustedBiddingUri(
216                                                     Uri.parse(
217                                                             trustedBiddingDataJson.getString(
218                                                                     "trustedBiddingUri")))
219                                             .build())
220                             .setBiddingLogicUri(Uri.parse(caJson.getString("biddingLogicUri")))
221                             .setAds(adDatas.build())
222                             .build());
223         }
224         return customAudienceBuilder.build();
225     }
226 
readAdSelectionConfig(String fileName)227     protected AdSelectionConfig readAdSelectionConfig(String fileName) throws Exception {
228         InputStream is = ApplicationProvider.getApplicationContext().getAssets().open(fileName);
229         JSONObject adSelectionConfigJson = new JSONObject(readString(is));
230         JSONArray buyersJson = adSelectionConfigJson.getJSONArray("custom_audience_buyers");
231         JSONObject perBuyerSignalsJson = adSelectionConfigJson.getJSONObject("per_buyer_signals");
232         is.close();
233 
234         ImmutableList.Builder<AdTechIdentifier> buyersBuilder = ImmutableList.builder();
235         ImmutableMap.Builder<AdTechIdentifier, AdSelectionSignals> perBuyerSignals =
236                 ImmutableMap.builder();
237         for (int i = 0; i < buyersJson.length(); i++) {
238             AdTechIdentifier buyer = AdTechIdentifier.fromString(buyersJson.getString(i));
239             buyersBuilder.add(buyer);
240             perBuyerSignals.put(
241                     buyer,
242                     AdSelectionSignals.fromString(perBuyerSignalsJson.getString(buyer.toString())));
243         }
244 
245         return new AdSelectionConfig.Builder()
246                 .setSeller(AdTechIdentifier.fromString(adSelectionConfigJson.getString("seller")))
247                 .setDecisionLogicUri(
248                         Uri.parse(adSelectionConfigJson.getString("decision_logic_uri")))
249                 .setAdSelectionSignals(
250                         AdSelectionSignals.fromString(
251                                 adSelectionConfigJson.getString("auction_signals")))
252                 .setSellerSignals(
253                         AdSelectionSignals.fromString(
254                                 adSelectionConfigJson.getString("seller_signals")))
255                 .setTrustedScoringSignalsUri(
256                         Uri.parse(adSelectionConfigJson.getString("trusted_scoring_signal_uri")))
257                 .setPerBuyerSignals(perBuyerSignals.build())
258                 .setCustomAudienceBuyers(buyersBuilder.build())
259                 .build();
260     }
261 
joinCustomAudiences(List<CustomAudience> customAudiences)262     private void joinCustomAudiences(List<CustomAudience> customAudiences) throws Exception {
263         for (CustomAudience ca : customAudiences) {
264             CUSTOM_AUDIENCE_CLIENT
265                     .joinCustomAudience(ca)
266                     .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
267         }
268     }
269 
generateLogLabel(String classSimpleName, String testName, long elapsedMs)270     private String generateLogLabel(String classSimpleName, String testName, long elapsedMs) {
271         return "("
272                 + "SELECT_ADS_LATENCY_"
273                 + classSimpleName
274                 + "#"
275                 + testName
276                 + ": "
277                 + elapsedMs
278                 + " ms)";
279     }
280 
readString(InputStream inputStream)281     private String readString(InputStream inputStream) throws IOException {
282         // readAllBytes() was added in API level 33. As a result, when this test executes on S-, we
283         // will need a workaround to process the InputStream.
284         return SdkLevel.isAtLeastT()
285                 ? new String(inputStream.readAllBytes())
286                 : CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
287     }
288 }
289