summaryrefslogtreecommitdiff
path: root/adservices/tests/perf/src/android/adservices/test
diff options
context:
space:
mode:
Diffstat (limited to 'adservices/tests/perf/src/android/adservices/test')
-rw-r--r--adservices/tests/perf/src/android/adservices/test/scenario/adservices/MeasurementRegisterCalls.java184
-rw-r--r--adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/LimitPerfTests.java609
-rw-r--r--adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/SelectAdsLatency.java301
-rw-r--r--adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/CustomAudienceSetupRule.java172
-rw-r--r--adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerDispatcherFactory.java264
-rw-r--r--adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRule.java289
-rw-r--r--adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRuleFactory.java35
7 files changed, 1854 insertions, 0 deletions
diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/MeasurementRegisterCalls.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/MeasurementRegisterCalls.java
new file mode 100644
index 0000000000..6cabc9700c
--- /dev/null
+++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/MeasurementRegisterCalls.java
@@ -0,0 +1,184 @@
+/*
+ * 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 android.adservices.test.scenario.adservices;
+
+import android.Manifest;
+import android.adservices.clients.measurement.MeasurementClient;
+import android.adservices.test.scenario.adservices.utils.MockWebServerRule;
+import android.content.Context;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.platform.test.scenario.annotation.Scenario;
+import android.provider.DeviceConfig;
+import android.support.test.uiautomator.UiDevice;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockWebServer;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/** Crystal Ball tests for Measurement API. */
+@Scenario
+@RunWith(JUnit4.class)
+public class MeasurementRegisterCalls {
+ protected static final Context sContext = ApplicationProvider.getApplicationContext();
+ private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool();
+ private static MeasurementClient sMeasurementClient;
+ private static UiDevice sDevice;
+
+ @BeforeClass
+ public static void setupDevicePropertiesAndInitializeClient() throws Exception {
+ sMeasurementClient =
+ new MeasurementClient.Builder()
+ .setContext(sContext)
+ .setExecutor(CALLBACK_EXECUTOR)
+ .build();
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.WRITE_DEVICE_CONFIG);
+
+ // Override consent manager behavior to give user consent.
+ getUiDevice()
+ .executeShellCommand("setprop debug.adservices.consent_manager_debug_mode true");
+
+ // Override the flag to allow current package to call APIs.
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_ADSERVICES,
+ "ppapi_app_allow_list",
+ "*",
+ /* makeDefault */ false);
+ }
+
+ @AfterClass
+ public static void resetDeviceProperties() throws Exception {
+ // Reset consent
+ getUiDevice()
+ .executeShellCommand("setprop debug.adservices.consent_manager_debug_mode false");
+
+ // Reset allowed packages.
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_ADSERVICES,
+ "ppapi_app_allow_list",
+ "null",
+ /* makeDefault */ false);
+ }
+
+ @Test
+ public void testRegisterSourceAndTriggerAndRunAttributionAndReporting() throws Exception {
+ // Create source registration response.
+ MockResponse sourceResponse = new MockResponse();
+ final JSONObject headerRegisterSource =
+ buildJson(
+ Map.of(
+ "source_event_id", 1,
+ "destination", "android-app://android.platform.test.scenario",
+ "priority", 1));
+ sourceResponse.addHeader("Attribution-Reporting-Register-Source", headerRegisterSource);
+ sourceResponse.setResponseCode(200);
+
+ // Create trigger registration response.
+ MockResponse triggerResponse = new MockResponse();
+ final JSONObject eventTriggerData =
+ buildJson(
+ Map.of(
+ "trigger_data", "1",
+ "priority", "101"));
+ final JSONArray eventTriggerDataList = buildJsonArray(List.of(eventTriggerData));
+ final JSONObject headerRegisterTrigger =
+ buildJson(Map.of("event_trigger_data", eventTriggerDataList));
+ triggerResponse.addHeader("Attribution-Reporting-Register-Trigger", headerRegisterTrigger);
+ triggerResponse.setResponseCode(200);
+
+ // Create report response.
+ MockResponse reportResponse = new MockResponse();
+ reportResponse.setResponseCode(200);
+
+ // Start mock web server.
+ List<MockResponse> responses = List.of(sourceResponse, triggerResponse, reportResponse);
+ MockWebServer mockWebServer =
+ MockWebServerRule.forHttps(
+ sContext, "adservices_test_server.p12", "adservices_test")
+ .startMockWebServer(responses);
+
+ URL url = mockWebServer.getUrl("/mockServer");
+
+ // Set the initial time to register the source and trigger.
+ getUiDevice().executeShellCommand("date -s 2022-08-01");
+
+ sMeasurementClient.registerSource(Uri.parse(url.toString()), null).get();
+ sMeasurementClient.registerTrigger(Uri.parse(url.toString())).get();
+ runAttributionJob();
+
+ // Advance the time so that generated report is within the reporting window.
+ getUiDevice().executeShellCommand("date -s 2022-09-01");
+ runReportingJob();
+ }
+
+ private void runAttributionJob() throws InterruptedException, IOException {
+ getUiDevice()
+ .executeShellCommand("cmd jobscheduler run -f com.google.android.adservices.api 5");
+ // Wait for attribution to complete.
+ SystemClock.sleep(2000);
+ }
+
+ private void runReportingJob() throws InterruptedException, IOException {
+ getUiDevice()
+ .executeShellCommand("cmd jobscheduler run -f com.google.android.adservices.api 3");
+ // Wait for reporting to complete.
+ SystemClock.sleep(2000);
+ }
+
+ private static JSONObject buildJson(Map<String, Object> fields) throws JSONException {
+ JSONObject json = new JSONObject();
+ for (Map.Entry<String, Object> entry : fields.entrySet()) {
+ json.put(entry.getKey(), entry.getValue());
+ }
+ return json;
+ }
+
+ private static JSONArray buildJsonArray(List<JSONObject> list) throws JSONException {
+ JSONArray json = new JSONArray();
+ for (int i = 0; i < list.size(); i++) {
+ json.put(i, list.get(i));
+ }
+ return json;
+ }
+
+ private static UiDevice getUiDevice() {
+ if (sDevice == null) {
+ sDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ }
+ return sDevice;
+ }
+}
diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/LimitPerfTests.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/LimitPerfTests.java
new file mode 100644
index 0000000000..0b15e94d60
--- /dev/null
+++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/LimitPerfTests.java
@@ -0,0 +1,609 @@
+/*
+ * 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 android.adservices.test.scenario.adservices.fledge;
+
+import android.Manifest;
+import android.adservices.adselection.AdSelectionConfig;
+import android.adservices.adselection.AdSelectionOutcome;
+import android.adservices.adselection.ReportImpressionRequest;
+import android.adservices.clients.adselection.AdSelectionClient;
+import android.adservices.clients.customaudience.AdvertisingCustomAudienceClient;
+import android.adservices.common.AdData;
+import android.adservices.common.AdSelectionSignals;
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.customaudience.CustomAudience;
+import android.adservices.customaudience.TrustedBiddingData;
+import android.adservices.test.scenario.adservices.utils.MockWebServerRule;
+import android.adservices.test.scenario.adservices.utils.MockWebServerRuleFactory;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.scenario.annotation.Scenario;
+import android.provider.DeviceConfig;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.ShellUtils;
+
+import com.google.common.collect.ImmutableList;
+import com.google.mockwebserver.Dispatcher;
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.RecordedRequest;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+@Scenario
+@RunWith(JUnit4.class)
+public class LimitPerfTests {
+
+ // The number of ms to sleep after killing the adservices process so it has time to recover
+ public static final long SLEEP_MS_AFTER_KILL = 2000L;
+ // Command to kill the adservices process
+ public static final String KILL_ADSERVICES_CMD =
+ "su 0 killall -9 com.google.android.adservices.api";
+ // Command prevent activity manager from backing off on restarting the adservices process
+ public static final String DISABLE_ADSERVICES_BACKOFF_CMD =
+ "am service-restart-backoff disable com.google.android.adservices.api";
+
+ public static final Duration CUSTOM_AUDIENCE_EXPIRE_IN = Duration.ofDays(1);
+ public static final Instant VALID_ACTIVATION_TIME = Instant.now();
+ public static final Instant VALID_EXPIRATION_TIME =
+ VALID_ACTIVATION_TIME.plus(CUSTOM_AUDIENCE_EXPIRE_IN);
+ public static final String VALID_NAME = "testCustomAudienceName";
+ public static final AdSelectionSignals VALID_USER_BIDDING_SIGNALS =
+ AdSelectionSignals.fromString("{'valid': 'yep', 'opaque': 'definitely'}");
+ public static final String VALID_TRUSTED_BIDDING_URI_PATH = "/trusted/bidding/";
+ public static final ArrayList<String> VALID_TRUSTED_BIDDING_KEYS =
+ new ArrayList<>(Arrays.asList("example", "valid", "list", "of", "keys"));
+ public static final AdTechIdentifier SELLER = AdTechIdentifier.fromString("localhost");
+ // Uri Constants
+ public static final String DECISION_LOGIC_PATH = "/decisionFragment";
+ public static final String TRUSTED_SCORING_SIGNAL_PATH = "/trustedScoringSignalsFragment";
+ public static final String CUSTOM_AUDIENCE_SHIRT = "ca_shirt";
+ public static final String CUSTOM_AUDIENCE_SHOES = "ca_shoe";
+ // TODO(b/244530379) Make compatible with multiple buyers
+ public static final AdTechIdentifier BUYER_1 = AdTechIdentifier.fromString("localhost");
+ public static final List<AdTechIdentifier> CUSTOM_AUDIENCE_BUYERS =
+ Collections.singletonList(BUYER_1);
+ public static final AdSelectionSignals AD_SELECTION_SIGNALS =
+ AdSelectionSignals.fromString("{\"ad_selection_signals\":1}");
+ public static final AdSelectionSignals SELLER_SIGNALS =
+ AdSelectionSignals.fromString("{\"test_seller_signals\":1}");
+ public static final Map<AdTechIdentifier, AdSelectionSignals> PER_BUYER_SIGNALS =
+ Map.of(BUYER_1, AdSelectionSignals.fromString("{\"buyer_signals\":1}"));
+ private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool();
+ // Time allowed by current test setup for APIs to respond
+ // setting a large value for perf testing, to avoid failing for large datasets
+ private static final int API_RESPONSE_TIMEOUT_SECONDS = 100;
+ private static final String BUYER_BIDDING_LOGIC_URI_PATH = "/buyer/bidding/logic/";
+ private static final String SELLER_REPORTING_PATH = "/reporting/seller";
+ private static final String BUYER_REPORTING_PATH = "/reporting/buyer";
+ private static final String DEFAULT_DECISION_LOGIC_JS =
+ "function scoreAd(ad, bid, auction_config, seller_signals,"
+ + " trusted_scoring_signals, contextual_signal, user_signal,"
+ + " custom_audience_signal) { \n"
+ + " return {'status': 0, 'score': bid };\n"
+ + "}\n"
+ + "function reportResult(ad_selection_config, render_uri, bid,"
+ + " contextual_signals) { \n"
+ + " return {'status': 0, 'results': {'signals_for_buyer':"
+ + " '{\"signals_for_buyer\":1}', 'reporting_uri': '"
+ + SELLER_REPORTING_PATH
+ + "' } };\n"
+ + "}";
+ private static final String DEFAULT_BIDDING_LOGIC_JS =
+ "function generateBid(ad, auction_signals, per_buyer_signals,"
+ + " trusted_bidding_signals, contextual_signals,"
+ + " custom_audience_signals) { \n"
+ + " return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };\n"
+ + "}\n"
+ + "function reportWin(ad_selection_signals, per_buyer_signals,"
+ + " signals_for_buyer, contextual_signals, custom_audience_signals) { \n"
+ + " return {'status': 0, 'results': {'reporting_uri': '"
+ + BUYER_REPORTING_PATH
+ + "' } };\n"
+ + "}";
+ private static final String CALCULATION_INTENSE_JS =
+ "for (let i = 1; i < 1000000000; i++) {\n" + " Math.sqrt(i);\n" + "}";
+ private static final String MEMORY_INTENSE_JS =
+ "var a = []\n" + "for (let i = 0; i < 10000; i++) {\n" + " a.push(i);" + "}";
+
+ private static final AdSelectionSignals TRUSTED_SCORING_SIGNALS =
+ AdSelectionSignals.fromString(
+ "{\n"
+ + "\t\"render_uri_1\": \"signals_for_1\",\n"
+ + "\t\"render_uri_2\": \"signals_for_2\"\n"
+ + "}");
+ private static final AdSelectionSignals TRUSTED_BIDDING_SIGNALS =
+ AdSelectionSignals.fromString(
+ "{\n"
+ + "\t\"example\": \"example\",\n"
+ + "\t\"valid\": \"Also valid\",\n"
+ + "\t\"list\": \"list\",\n"
+ + "\t\"of\": \"of\",\n"
+ + "\t\"keys\": \"trusted bidding signal Values\"\n"
+ + "}");
+ private static final String AD_URI_PREFIX = "/adverts/123/";
+ private static final int DELAY_TO_AVOID_THROTTLE_MS = 1001;
+ protected final Context mContext = ApplicationProvider.getApplicationContext();
+ private final AdSelectionClient mAdSelectionClient =
+ new AdSelectionClient.Builder()
+ .setContext(mContext)
+ .setExecutor(CALLBACK_EXECUTOR)
+ .build();
+ private final AdvertisingCustomAudienceClient mCustomAudienceClient =
+ new AdvertisingCustomAudienceClient.Builder()
+ .setContext(mContext)
+ .setExecutor(CALLBACK_EXECUTOR)
+ .build();
+ @Rule public MockWebServerRule mMockWebServerRule = MockWebServerRuleFactory.createForHttps();
+ private Dispatcher mDefaultDispatcher;
+
+ @BeforeClass
+ public static void setupBeforeClass() {
+ // Disable backoff since we will be killing the process between tests
+ ShellUtils.runShellCommand(DISABLE_ADSERVICES_BACKOFF_CMD);
+ InstrumentationRegistry.getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity(Manifest.permission.WRITE_DEVICE_CONFIG);
+ // TODO(b/245585645) Mark true for the heap size enforcement after installing M105 library
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_ADSERVICES,
+ "fledge_js_isolate_enforce_max_heap_size",
+ "false",
+ true);
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_ADSERVICES, "disable_fledge_enrollment_check", "true", true);
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_ADSERVICES, "ppapi_app_allow_list", "*", true);
+ }
+
+ public static Uri getUri(String name, String path) {
+ return Uri.parse("https://" + name + path);
+ }
+
+ @Before
+ public void setup() throws InterruptedException {
+ ShellUtils.runShellCommand(KILL_ADSERVICES_CMD);
+ Thread.sleep(SLEEP_MS_AFTER_KILL);
+ mDefaultDispatcher =
+ new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ if (DECISION_LOGIC_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody(DEFAULT_DECISION_LOGIC_JS);
+ } else if (BUYER_BIDDING_LOGIC_URI_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody(DEFAULT_BIDDING_LOGIC_JS);
+ } else if (BUYER_REPORTING_PATH.equals(request.getPath())
+ || SELLER_REPORTING_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody("");
+ } else if (request.getPath().startsWith(TRUSTED_SCORING_SIGNAL_PATH)) {
+ return new MockResponse().setBody(TRUSTED_SCORING_SIGNALS.toString());
+ } else if (request.getPath().startsWith(VALID_TRUSTED_BIDDING_URI_PATH)) {
+ return new MockResponse().setBody(TRUSTED_BIDDING_SIGNALS.toString());
+ }
+ return new MockResponse().setResponseCode(404);
+ }
+ };
+ }
+
+ @Test
+ public void test_joinCustomAudience_success() throws Exception {
+ CustomAudience ca =
+ createCustomAudience(
+ BUYER_1, CUSTOM_AUDIENCE_SHOES, Collections.singletonList(1.0));
+ addDelayToAvoidThrottle();
+ mCustomAudienceClient
+ .joinCustomAudience(ca)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ addDelayToAvoidThrottle();
+ mCustomAudienceClient
+ .leaveCustomAudience(ca.getBuyer(), ca.getName())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+
+ @Test
+ public void testAdSelectionAndReporting_normalFlow_success() throws Exception {
+ List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
+ List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
+ CustomAudience customAudience1 =
+ createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHOES, bidsForBuyer1);
+
+ // TODO(b/244530379) Make compatible with multiple buyers
+ CustomAudience customAudience2 =
+ createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHIRT, bidsForBuyer2);
+ List<CustomAudience> customAudienceList = Arrays.asList(customAudience1, customAudience2);
+
+ mMockWebServerRule.startMockWebServer(mDefaultDispatcher);
+
+ for (CustomAudience ca : customAudienceList) {
+ addDelayToAvoidThrottle();
+ mCustomAudienceClient
+ .joinCustomAudience(ca)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+
+ // Running ad selection and asserting that the outcome is returned in < 10 seconds
+ addDelayToAvoidThrottle();
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ // The winning ad should be ad3 from CA shirt
+ Assert.assertEquals(
+ "Ad selection outcome is not expected",
+ createExpectedWinningUri(BUYER_1, CUSTOM_AUDIENCE_SHIRT, 3),
+ outcome.getRenderUri());
+
+ ReportImpressionRequest reportImpressionRequest =
+ new ReportImpressionRequest(outcome.getAdSelectionId(), createAdSelectionConfig());
+
+ // Performing reporting, and asserting that no exception is thrown
+ addDelayToAvoidThrottle();
+ mAdSelectionClient
+ .reportImpression(reportImpressionRequest)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ // Cleanup
+ for (CustomAudience ca : customAudienceList) {
+ addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS);
+ mCustomAudienceClient
+ .leaveCustomAudience(ca.getBuyer(), ca.getName())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ public void testAdSelectionAndReporting_executionHeavyJS_success() throws Exception {
+ List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
+ List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
+ CustomAudience customAudience1 =
+ createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHOES, bidsForBuyer1);
+
+ // TODO(b/244530379) Make compatible with multiple buyers
+ CustomAudience customAudience2 =
+ createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHIRT, bidsForBuyer2);
+ List<CustomAudience> customAudienceList = Arrays.asList(customAudience1, customAudience2);
+
+ String calculation_intense_logic_js =
+ "function scoreAd(ad, bid, auction_config, seller_signals,"
+ + " trusted_scoring_signals, contextual_signal, user_signal,"
+ + " custom_audience_signal) { \n"
+ + CALCULATION_INTENSE_JS
+ + " return {'status': 0, 'score': bid };\n"
+ + "}\n"
+ + "function reportResult(ad_selection_config, render_uri, bid,"
+ + " contextual_signals) { \n"
+ + " return {'status': 0, 'results': {'signals_for_buyer':"
+ + " '{\"signals_for_buyer\":1}', 'reporting_uri': '"
+ + SELLER_REPORTING_PATH
+ + "' } };\n"
+ + "}";
+
+ Dispatcher dispatcher =
+ new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ if (DECISION_LOGIC_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody(calculation_intense_logic_js);
+ } else if (BUYER_BIDDING_LOGIC_URI_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody(DEFAULT_BIDDING_LOGIC_JS);
+ } else if (BUYER_REPORTING_PATH.equals(request.getPath())
+ || SELLER_REPORTING_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody("");
+ } else if (request.getPath().startsWith(TRUSTED_SCORING_SIGNAL_PATH)) {
+ return new MockResponse().setBody(TRUSTED_SCORING_SIGNALS.toString());
+ } else if (request.getPath().startsWith(VALID_TRUSTED_BIDDING_URI_PATH)) {
+ return new MockResponse().setBody(TRUSTED_BIDDING_SIGNALS.toString());
+ }
+ return new MockResponse().setResponseCode(404);
+ }
+ };
+
+ mMockWebServerRule.startMockWebServer(dispatcher);
+
+ for (CustomAudience ca : customAudienceList) {
+ addDelayToAvoidThrottle();
+ mCustomAudienceClient
+ .joinCustomAudience(ca)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+
+ // Running ad selection and asserting that the outcome is returned in < 10 seconds
+ addDelayToAvoidThrottle();
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ // The winning ad should be ad3 from CA shirt
+ Assert.assertEquals(
+ "Ad selection outcome is not expected",
+ createExpectedWinningUri(BUYER_1, CUSTOM_AUDIENCE_SHIRT, 3),
+ outcome.getRenderUri());
+
+ ReportImpressionRequest reportImpressionRequest =
+ new ReportImpressionRequest(outcome.getAdSelectionId(), createAdSelectionConfig());
+
+ // Performing reporting, and asserting that no exception is thrown
+ addDelayToAvoidThrottle();
+ mAdSelectionClient
+ .reportImpression(reportImpressionRequest)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ // Cleanup
+ for (CustomAudience ca : customAudienceList) {
+ addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS);
+ mCustomAudienceClient
+ .leaveCustomAudience(ca.getBuyer(), ca.getName())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ public void testAdSelectionAndReporting_memoryHeavyJS_success() throws Exception {
+ List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
+ List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
+ CustomAudience customAudience1 =
+ createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHOES, bidsForBuyer1);
+
+ // TODO(b/244530379) Make compatible with multiple buyers
+ CustomAudience customAudience2 =
+ createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHIRT, bidsForBuyer2);
+ List<CustomAudience> customAudienceList = Arrays.asList(customAudience1, customAudience2);
+
+ String memory_intense_logic_js =
+ "function scoreAd(ad, bid, auction_config, seller_signals,"
+ + " trusted_scoring_signals, contextual_signal, user_signal,"
+ + " custom_audience_signal) { \n"
+ + MEMORY_INTENSE_JS
+ + " return {'status': 0, 'score': bid };\n"
+ + "}\n"
+ + "function reportResult(ad_selection_config, render_uri, bid,"
+ + " contextual_signals) { \n"
+ + " return {'status': 0, 'results': {'signals_for_buyer':"
+ + " '{\"signals_for_buyer\":1}', 'reporting_uri': '"
+ + SELLER_REPORTING_PATH
+ + "' } };\n"
+ + "}";
+
+ Dispatcher dispatcher =
+ new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ if (DECISION_LOGIC_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody(memory_intense_logic_js);
+ } else if (BUYER_BIDDING_LOGIC_URI_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody(DEFAULT_BIDDING_LOGIC_JS);
+ } else if (BUYER_REPORTING_PATH.equals(request.getPath())
+ || SELLER_REPORTING_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody("");
+ } else if (request.getPath().startsWith(TRUSTED_SCORING_SIGNAL_PATH)) {
+ return new MockResponse().setBody(TRUSTED_SCORING_SIGNALS.toString());
+ } else if (request.getPath().startsWith(VALID_TRUSTED_BIDDING_URI_PATH)) {
+ return new MockResponse().setBody(TRUSTED_BIDDING_SIGNALS.toString());
+ }
+ return new MockResponse().setResponseCode(404);
+ }
+ };
+
+ mMockWebServerRule.startMockWebServer(dispatcher);
+
+ for (CustomAudience ca : customAudienceList) {
+ addDelayToAvoidThrottle();
+ mCustomAudienceClient
+ .joinCustomAudience(ca)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+
+ // Running ad selection and asserting that the outcome is returned in < 10 seconds
+ addDelayToAvoidThrottle();
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ // The winning ad should be ad3 from CA shirt
+ Assert.assertEquals(
+ "Ad selection outcome is not expected",
+ createExpectedWinningUri(BUYER_1, CUSTOM_AUDIENCE_SHIRT, 3),
+ outcome.getRenderUri());
+
+ ReportImpressionRequest reportImpressionRequest =
+ new ReportImpressionRequest(outcome.getAdSelectionId(), createAdSelectionConfig());
+
+ // Performing reporting, and asserting that no exception is thrown
+ addDelayToAvoidThrottle();
+ mAdSelectionClient
+ .reportImpression(reportImpressionRequest)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ // Cleanup
+ for (CustomAudience ca : customAudienceList) {
+ addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS);
+ mCustomAudienceClient
+ .leaveCustomAudience(ca.getBuyer(), ca.getName())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ public void testAdSelectionAndReporting_multipleCustomAudienceList_success() throws Exception {
+ List<Double> bidsForBuyer1 = ImmutableList.of(1.1, 2.2);
+ List<Double> bidsForBuyer2 = ImmutableList.of(4.5, 6.7, 10.0);
+ CustomAudience customAudience1 =
+ createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHOES, bidsForBuyer1);
+
+ // TODO(b/244530379) Make compatible with multiple buyers
+ CustomAudience customAudience2 =
+ createCustomAudience(BUYER_1, CUSTOM_AUDIENCE_SHIRT, bidsForBuyer2);
+ List<CustomAudience> customAudienceList = new ArrayList<>();
+ customAudienceList.add(customAudience1);
+ customAudienceList.add(customAudience2);
+
+ // Create multiple generic custom audience entries
+ for (int i = 1; i <= 48; i++) {
+ CustomAudience customAudience =
+ createCustomAudience(BUYER_1, "GENERIC_CA_" + i, bidsForBuyer1);
+ customAudienceList.add(customAudience);
+ }
+ mMockWebServerRule.startMockWebServer(mDefaultDispatcher);
+
+ for (CustomAudience ca : customAudienceList) {
+ addDelayToAvoidThrottle();
+ mCustomAudienceClient
+ .joinCustomAudience(ca)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+
+ // Running ad selection and asserting that the outcome is returned in < 10 seconds
+ addDelayToAvoidThrottle();
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ // The winning ad should be ad3 from CA shirt
+ Assert.assertEquals(
+ "Ad selection outcome is not expected",
+ createExpectedWinningUri(BUYER_1, CUSTOM_AUDIENCE_SHIRT, 3),
+ outcome.getRenderUri());
+
+ ReportImpressionRequest reportImpressionRequest =
+ new ReportImpressionRequest(outcome.getAdSelectionId(), createAdSelectionConfig());
+
+ // Performing reporting, and asserting that no exception is thrown
+ addDelayToAvoidThrottle();
+ mAdSelectionClient
+ .reportImpression(reportImpressionRequest)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ // Cleanup
+ for (CustomAudience ca : customAudienceList) {
+ addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS);
+ mCustomAudienceClient
+ .leaveCustomAudience(ca.getBuyer(), ca.getName())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ }
+
+ private void addDelayToAvoidThrottle() throws InterruptedException {
+ addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS);
+ }
+
+ private void addDelayToAvoidThrottle(int delayValueMs) throws InterruptedException {
+ if (delayValueMs > 0) {
+ Thread.sleep(delayValueMs);
+ }
+ }
+
+ private CustomAudience createCustomAudience(
+ final AdTechIdentifier buyer,
+ String name,
+ List<Double> bids,
+ Instant activationTime,
+ Instant expirationTime) {
+ // Generate ads for with bids provided
+ List<AdData> ads = new ArrayList<>();
+
+ // Create ads with the custom audience name and bid number as the ad URI
+ // Add the bid value to the metadata
+ for (int i = 0; i < bids.size(); i++) {
+ ads.add(
+ new AdData.Builder()
+ .setRenderUri(
+ getUri(
+ buyer.toString(),
+ AD_URI_PREFIX + name + "/ad" + (i + 1)))
+ .setMetadata("{\"result\":" + bids.get(i) + "}")
+ .build());
+ }
+
+ return new CustomAudience.Builder()
+ .setBuyer(buyer)
+ .setName(name)
+ .setActivationTime(activationTime)
+ .setExpirationTime(expirationTime)
+ .setDailyUpdateUri(getValidDailyUpdateUriByBuyer(buyer))
+ .setUserBiddingSignals(VALID_USER_BIDDING_SIGNALS)
+ .setTrustedBiddingData(getValidTrustedBiddingDataByBuyer(buyer))
+ .setBiddingLogicUri(mMockWebServerRule.uriForPath(BUYER_BIDDING_LOGIC_URI_PATH))
+ .setAds(ads)
+ .build();
+ }
+
+ private CustomAudience createCustomAudience(
+ final AdTechIdentifier buyer, String name, List<Double> bids) {
+ return createCustomAudience(
+ buyer, name, bids, VALID_ACTIVATION_TIME, VALID_EXPIRATION_TIME);
+ }
+
+ private AdSelectionConfig createAdSelectionConfig() {
+ return new AdSelectionConfig.Builder()
+ .setSeller(SELLER)
+ .setDecisionLogicUri(mMockWebServerRule.uriForPath(DECISION_LOGIC_PATH))
+ .setCustomAudienceBuyers(CUSTOM_AUDIENCE_BUYERS)
+ .setAdSelectionSignals(AD_SELECTION_SIGNALS)
+ .setSellerSignals(SELLER_SIGNALS)
+ .setPerBuyerSignals(PER_BUYER_SIGNALS)
+ .setTrustedScoringSignalsUri(
+ mMockWebServerRule.uriForPath(TRUSTED_SCORING_SIGNAL_PATH))
+ // TODO(b/244530379) Make compatible with multiple buyers
+ .setCustomAudienceBuyers(Collections.singletonList(BUYER_1))
+ .build();
+ }
+
+ private Uri createExpectedWinningUri(
+ AdTechIdentifier buyer, String customAudienceName, int adNumber) {
+ return getUri(buyer.toString(), AD_URI_PREFIX + customAudienceName + "/ad" + adNumber);
+ }
+
+ // TODO(b/244530379) Make compatible with multiple buyers
+ public Uri getValidDailyUpdateUriByBuyer(AdTechIdentifier buyer) {
+ return mMockWebServerRule.uriForPath("/update");
+ }
+
+ public TrustedBiddingData getValidTrustedBiddingDataByBuyer(AdTechIdentifier buyer) {
+ return new TrustedBiddingData.Builder()
+ .setTrustedBiddingKeys(VALID_TRUSTED_BIDDING_KEYS)
+ .setTrustedBiddingUri(getValidTrustedBiddingUriByBuyer(buyer))
+ .build();
+ }
+
+ // TODO(b/244530379) Make compatible with multiple buyers
+ public Uri getValidTrustedBiddingUriByBuyer(AdTechIdentifier buyer) {
+ return mMockWebServerRule.uriForPath(VALID_TRUSTED_BIDDING_URI_PATH);
+ }
+}
diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/SelectAdsLatency.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/SelectAdsLatency.java
new file mode 100644
index 0000000000..5af52346a9
--- /dev/null
+++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/fledge/SelectAdsLatency.java
@@ -0,0 +1,301 @@
+/*
+ * 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 src.android.adservices.test.scenario.adservices.fledge;
+
+import android.adservices.adselection.AdSelectionConfig;
+import android.adservices.adselection.AdSelectionOutcome;
+import android.adservices.clients.adselection.AdSelectionClient;
+import android.adservices.clients.customaudience.AdvertisingCustomAudienceClient;
+import android.adservices.common.AdSelectionSignals;
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.test.scenario.adservices.utils.CustomAudienceSetupRule;
+import android.adservices.test.scenario.adservices.utils.MockWebServerDispatcherFactory;
+import android.adservices.test.scenario.adservices.utils.MockWebServerRule;
+import android.adservices.test.scenario.adservices.utils.MockWebServerRuleFactory;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.scenario.annotation.Scenario;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.compatibility.common.util.ShellUtils;
+
+import com.google.common.base.Stopwatch;
+import com.google.common.base.Ticker;
+
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/** Tests to compute latency runs for on device ad selection. */
+
+// TODO(b/251802548): Consider parameterizing the tests.
+@Scenario
+@RunWith(JUnit4.class)
+public class SelectAdsLatency {
+ private static final String TAG = "SelectAds";
+
+ private static final AdTechIdentifier BUYER = AdTechIdentifier.fromString("localhost");
+ private static final List<AdTechIdentifier> CUSTOM_AUDIENCE_BUYERS =
+ Collections.singletonList(BUYER);
+ private static final AdSelectionSignals AD_SELECTION_SIGNALS =
+ AdSelectionSignals.fromString("{\"ad_selection_signals\":1}");
+ private static final AdSelectionSignals SELLER_SIGNALS =
+ AdSelectionSignals.fromString("{\"test_seller_signals\":1}");
+ private static final Map<AdTechIdentifier, AdSelectionSignals> PER_BUYER_SIGNALS =
+ Map.of(BUYER, AdSelectionSignals.fromString("{\"buyer_signals\":1}"));
+ private static final AdTechIdentifier SELLER = AdTechIdentifier.fromString("localhost");
+ private static final int API_RESPONSE_TIMEOUT_SECONDS = 100;
+ private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool();
+
+ // Estimates from
+ // https://docs.google.com/spreadsheets/d/1EP_cwBbwYI-NMro0Qq5uif1krwjIQhjK8fjOu15j7hQ/edit?usp=sharing&resourcekey=0-A67kzEnAKKz1k7qpshSedg
+ private static final int NUMBER_OF_CUSTOM_AUDIENCES_MEDIUM = 10;
+ private static final int NUMBER_OF_CUSTOM_AUDIENCES_LARGE = 50;
+ private static final int NUMBER_ADS_PER_CA_MEDIUM = 5;
+ private static final int NUMBER_ADS_PER_CA_LARGE = 10;
+
+ private static final String LOG_LABEL_P50_5G = "SELECT_ADS_LATENCY_P50_5G";
+ private static final String LOG_LABEL_P50_4GPLUS = "SELECT_ADS_LATENCY_P50_4GPLUS";
+ private static final String LOG_LABEL_P50_4G = "SELECT_ADS_LATENCY_P50_4G";
+ private static final String LOG_LABEL_P90_5G = "SELECT_ADS_LATENCY_P90_5G";
+ private static final String LOG_LABEL_P90_4GPLUS = "SELECT_ADS_LATENCY_P90_4GPLUS";
+ private static final String LOG_LABEL_P90_4G = "SELECT_ADS_LATENCY_P90_4G";
+
+ private static final String AD_SELECTION_FAILURE_MESSAGE =
+ "Ad selection outcome is not expected";
+
+ protected final Context mContext = ApplicationProvider.getApplicationContext();
+ private final AdSelectionClient mAdSelectionClient =
+ new AdSelectionClient.Builder()
+ .setContext(mContext)
+ .setExecutor(CALLBACK_EXECUTOR)
+ .build();
+ private final AdvertisingCustomAudienceClient mCustomAudienceClient =
+ new AdvertisingCustomAudienceClient.Builder()
+ .setContext(mContext)
+ .setExecutor(CALLBACK_EXECUTOR)
+ .build();
+ @Rule public MockWebServerRule mMockWebServerRule = MockWebServerRuleFactory.createForHttps();
+
+ @Rule
+ public CustomAudienceSetupRule mCustomAudienceSetupRule =
+ new CustomAudienceSetupRule(mCustomAudienceClient, mMockWebServerRule);
+
+ private final Ticker mTicker =
+ new Ticker() {
+ public long read() {
+ return android.os.SystemClock.elapsedRealtimeNanos();
+ }
+ };
+
+ @BeforeClass
+ public static void setupBeforeClass() {
+ ShellUtils.runShellCommand("su 0 killall -9 com.google.android.adservices.api");
+ }
+
+ @Test
+ public void selectAds_p50_5G() throws Exception {
+ mMockWebServerRule.createMockWebServer();
+ mMockWebServerRule.startCreatedMockWebServer(
+ MockWebServerDispatcherFactory.create5Gp50LatencyDispatcher(mMockWebServerRule));
+ mCustomAudienceSetupRule.populateCustomAudiences(
+ NUMBER_OF_CUSTOM_AUDIENCES_MEDIUM, NUMBER_ADS_PER_CA_MEDIUM);
+ Stopwatch timer = Stopwatch.createStarted(mTicker);
+
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ timer.stop();
+
+ // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this
+ // with a perfetto trace collector because it won't be affected by write to logcat latency.
+ Log.i(TAG, "(" + LOG_LABEL_P50_5G + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)");
+ Assert.assertEquals(
+ AD_SELECTION_FAILURE_MESSAGE,
+ createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_MEDIUM),
+ outcome.getRenderUri());
+ }
+
+ @Test
+ public void selectAds_p50_4GPlus() throws Exception {
+ mMockWebServerRule.createMockWebServer();
+ mMockWebServerRule.startCreatedMockWebServer(
+ MockWebServerDispatcherFactory.create4GPlusp50LatencyDispatcher(
+ mMockWebServerRule));
+ mCustomAudienceSetupRule.populateCustomAudiences(
+ NUMBER_OF_CUSTOM_AUDIENCES_MEDIUM, NUMBER_ADS_PER_CA_MEDIUM);
+
+ Stopwatch timer = Stopwatch.createStarted(mTicker);
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ timer.stop();
+
+ // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this
+ // with a perfetto trace collector because it won't be affected by write to logcat latency.
+ Log.i(
+ TAG,
+ "(" + LOG_LABEL_P50_4GPLUS + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)");
+ Assert.assertEquals(
+ AD_SELECTION_FAILURE_MESSAGE,
+ createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_MEDIUM),
+ outcome.getRenderUri());
+ }
+
+ @Test
+ public void selectAds_p50_4G() throws Exception {
+ mMockWebServerRule.createMockWebServer();
+ mMockWebServerRule.startCreatedMockWebServer(
+ MockWebServerDispatcherFactory.create4Gp50LatencyDispatcher(mMockWebServerRule));
+ mCustomAudienceSetupRule.populateCustomAudiences(
+ NUMBER_OF_CUSTOM_AUDIENCES_MEDIUM, NUMBER_ADS_PER_CA_MEDIUM);
+
+ Stopwatch timer = Stopwatch.createStarted(mTicker);
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ timer.stop();
+
+ // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this
+ // with a perfetto trace collector because it won't be affected by write to logcat latency.
+ Log.i(TAG, "(" + LOG_LABEL_P50_4G + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)");
+ Assert.assertEquals(
+ AD_SELECTION_FAILURE_MESSAGE,
+ createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_MEDIUM),
+ outcome.getRenderUri());
+ }
+
+ @Test
+ public void selectAds_p90_5G() throws Exception {
+ mMockWebServerRule.createMockWebServer();
+ mMockWebServerRule.startCreatedMockWebServer(
+ MockWebServerDispatcherFactory.create5Gp90LatencyDispatcher(mMockWebServerRule));
+ mCustomAudienceSetupRule.populateCustomAudiences(
+ NUMBER_OF_CUSTOM_AUDIENCES_LARGE, NUMBER_ADS_PER_CA_LARGE);
+
+ Stopwatch timer = Stopwatch.createStarted(mTicker);
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ timer.stop();
+
+ // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this
+ // with a perfetto trace collector because it won't be affected by write to logcat latency.
+ Log.i(TAG, "(" + LOG_LABEL_P90_5G + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)");
+ Assert.assertEquals(
+ AD_SELECTION_FAILURE_MESSAGE,
+ createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_LARGE),
+ outcome.getRenderUri());
+ }
+
+ @Test
+ public void selectAds_p90_4G() throws Exception {
+ mMockWebServerRule.createMockWebServer();
+ mMockWebServerRule.startCreatedMockWebServer(
+ MockWebServerDispatcherFactory.create4Gp90LatencyDispatcher(mMockWebServerRule));
+ mCustomAudienceSetupRule.populateCustomAudiences(
+ NUMBER_OF_CUSTOM_AUDIENCES_LARGE, NUMBER_ADS_PER_CA_LARGE);
+ Stopwatch timer = Stopwatch.createStarted(mTicker);
+
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ timer.stop();
+ // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this
+ // with a perfetto trace collector because it won't be affected by write to logcat latency.
+ Log.i(TAG, "(" + LOG_LABEL_P90_4G + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)");
+ Assert.assertEquals(
+ AD_SELECTION_FAILURE_MESSAGE,
+ createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_LARGE),
+ outcome.getRenderUri());
+ }
+
+ @Test
+ public void selectAds_p90_4GPlus() throws Exception {
+ mMockWebServerRule.createMockWebServer();
+ mMockWebServerRule.startCreatedMockWebServer(
+ MockWebServerDispatcherFactory.create4GPlusp90LatencyDispatcher(
+ mMockWebServerRule));
+ mCustomAudienceSetupRule.populateCustomAudiences(
+ NUMBER_OF_CUSTOM_AUDIENCES_LARGE, NUMBER_ADS_PER_CA_LARGE);
+
+ Stopwatch timer = Stopwatch.createStarted(mTicker);
+ AdSelectionOutcome outcome =
+ mAdSelectionClient
+ .selectAds(createAdSelectionConfig())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+ timer.stop();
+ // TODO(b/250851601): Currently we use logcat latency collector. Consider replacing this
+ // with a perfetto trace collector because it won't be affected by write to logcat latency.
+ Log.i(
+ TAG,
+ "(" + LOG_LABEL_P90_4GPLUS + ": " + timer.elapsed(TimeUnit.MILLISECONDS) + " ms)");
+ Assert.assertEquals(
+ AD_SELECTION_FAILURE_MESSAGE,
+ createExpectedWinningUri(BUYER, "GENERIC_CA_1", NUMBER_ADS_PER_CA_LARGE),
+ outcome.getRenderUri());
+ }
+
+ private static Uri getUri(String name, String path) {
+ return Uri.parse("https://" + name + path);
+ }
+
+ private Uri createExpectedWinningUri(
+ AdTechIdentifier buyer, String customAudienceName, int adNumber) {
+ return getUri(buyer.toString(), "/adverts/123/" + customAudienceName + "/ad" + adNumber);
+ }
+
+ private AdSelectionConfig createAdSelectionConfig() {
+ return new AdSelectionConfig.Builder()
+ .setSeller(SELLER)
+ .setDecisionLogicUri(
+ mMockWebServerRule.uriForPath(
+ MockWebServerDispatcherFactory.getDecisionLogicPath()))
+ // TODO(b/244530379) Make compatible with multiple buyers
+ .setCustomAudienceBuyers(CUSTOM_AUDIENCE_BUYERS)
+ .setAdSelectionSignals(AD_SELECTION_SIGNALS)
+ .setSellerSignals(SELLER_SIGNALS)
+ .setPerBuyerSignals(PER_BUYER_SIGNALS)
+ .setTrustedScoringSignalsUri(
+ mMockWebServerRule.uriForPath(
+ MockWebServerDispatcherFactory.getTrustedScoringSignalPath()))
+ .build();
+ }
+}
diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/CustomAudienceSetupRule.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/CustomAudienceSetupRule.java
new file mode 100644
index 0000000000..a9623520f1
--- /dev/null
+++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/CustomAudienceSetupRule.java
@@ -0,0 +1,172 @@
+/*
+ * 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 android.adservices.test.scenario.adservices.utils;
+
+import android.adservices.clients.customaudience.AdvertisingCustomAudienceClient;
+import android.adservices.common.AdData;
+import android.adservices.common.AdSelectionSignals;
+import android.adservices.common.AdTechIdentifier;
+import android.adservices.customaudience.CustomAudience;
+import android.adservices.customaudience.TrustedBiddingData;
+import android.net.Uri;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class CustomAudienceSetupRule implements TestRule {
+
+ private static final AdTechIdentifier BUYER = AdTechIdentifier.fromString("localhost");
+ private static final String AD_URI_PREFIX = "/adverts/123/";
+ private static final int DELAY_TO_AVOID_THROTTLE_MS = 1001;
+ private static final int API_RESPONSE_TIMEOUT_SECONDS = 100;
+ private static final Duration CUSTOM_AUDIENCE_EXPIRE_IN = Duration.ofDays(1);
+ private static final Instant VALID_ACTIVATION_TIME = Instant.now();
+ private static final Instant VALID_EXPIRATION_TIME =
+ VALID_ACTIVATION_TIME.plus(CUSTOM_AUDIENCE_EXPIRE_IN);
+ private static final AdSelectionSignals VALID_USER_BIDDING_SIGNALS =
+ AdSelectionSignals.fromString("{'valid': 'yep', 'opaque': 'definitely'}");
+ private final List<CustomAudience> mCustomAudiences;
+ private final AdvertisingCustomAudienceClient mAdvertisingCustomAudienceClient;
+ private final android.adservices.test.scenario.adservices.utils.MockWebServerRule
+ mMockWebServerRule;
+
+ public CustomAudienceSetupRule(
+ AdvertisingCustomAudienceClient advertisingCustomAudienceClient,
+ MockWebServerRule mockWebServerRule) {
+ mAdvertisingCustomAudienceClient = advertisingCustomAudienceClient;
+ mMockWebServerRule = mockWebServerRule;
+ mCustomAudiences = new ArrayList<>();
+ }
+
+ private static Uri getUri(String name, String path) {
+ return Uri.parse("https://" + name + path);
+ }
+
+ public void populateCustomAudiences(
+ int numberOfCustomAudiences, int numberOfAdsPerCustomAudiences) throws Exception {
+ List<Double> bidsForBuyer = new ArrayList<>();
+ for (int i = 1; i <= numberOfAdsPerCustomAudiences; i++) {
+ bidsForBuyer.add(i + 0.1);
+ }
+ // Create multiple generic custom audience entries
+ for (int i = 1; i <= numberOfCustomAudiences; i++) {
+ CustomAudience customAudience =
+ createCustomAudience(BUYER, "GENERIC_CA_" + i, bidsForBuyer);
+ mCustomAudiences.add(customAudience);
+ }
+
+ for (CustomAudience ca : mCustomAudiences) {
+ addDelayToAvoidThrottle();
+ mAdvertisingCustomAudienceClient
+ .joinCustomAudience(ca)
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ try {
+ base.evaluate();
+ } finally {
+ if (mCustomAudiences != null) {
+ for (CustomAudience ca : mCustomAudiences) {
+ addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS);
+ mAdvertisingCustomAudienceClient
+ .leaveCustomAudience(ca.getBuyer(), ca.getName())
+ .get(API_RESPONSE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ }
+ mCustomAudiences.clear();
+ }
+ }
+ }
+ };
+ }
+
+ private CustomAudience createCustomAudience(
+ final AdTechIdentifier buyer,
+ String name,
+ List<Double> bids,
+ Instant activationTime,
+ Instant expirationTime) {
+ // Generate ads for with bids provided
+ List<AdData> ads = new ArrayList<>();
+
+ // Create ads with the custom audience name and bid number as the ad URI
+ // Add the bid value to the metadata
+ for (int i = 0; i < bids.size(); i++) {
+ ads.add(
+ new AdData.Builder()
+ .setRenderUri(
+ getUri(
+ buyer.toString(),
+ AD_URI_PREFIX + name + "/ad" + (i + 1)))
+ .setMetadata("{\"result\":" + bids.get(i) + "}")
+ .build());
+ }
+
+ return new CustomAudience.Builder()
+ .setBuyer(buyer)
+ .setName(name)
+ .setActivationTime(activationTime)
+ .setExpirationTime(expirationTime)
+ .setDailyUpdateUri(mMockWebServerRule.uriForPath("/update"))
+ .setUserBiddingSignals(VALID_USER_BIDDING_SIGNALS)
+ .setTrustedBiddingData(
+ getValidTrustedBiddingDataByBuyer(
+ mMockWebServerRule.uriForPath(
+ MockWebServerDispatcherFactory
+ .getTrustedBiddingSignalsPath())))
+ .setBiddingLogicUri(
+ mMockWebServerRule.uriForPath(
+ MockWebServerDispatcherFactory.getBiddingLogicUriPath()))
+ .setAds(ads)
+ .build();
+ }
+
+ private CustomAudience createCustomAudience(
+ final AdTechIdentifier buyer, String name, List<Double> bids) {
+ return createCustomAudience(
+ buyer, name, bids, VALID_ACTIVATION_TIME, VALID_EXPIRATION_TIME);
+ }
+
+ private TrustedBiddingData getValidTrustedBiddingDataByBuyer(Uri validTrustedBiddingUri) {
+ return new TrustedBiddingData.Builder()
+ .setTrustedBiddingKeys(MockWebServerDispatcherFactory.getValidTrustedBiddingKeys())
+ .setTrustedBiddingUri(validTrustedBiddingUri)
+ .build();
+ }
+
+ private void addDelayToAvoidThrottle() throws InterruptedException {
+ addDelayToAvoidThrottle(DELAY_TO_AVOID_THROTTLE_MS);
+ }
+
+ private void addDelayToAvoidThrottle(int delayValueMs) throws InterruptedException {
+ if (delayValueMs > 0) {
+ Thread.sleep(delayValueMs);
+ }
+ }
+}
diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerDispatcherFactory.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerDispatcherFactory.java
new file mode 100644
index 0000000000..14005df1b3
--- /dev/null
+++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerDispatcherFactory.java
@@ -0,0 +1,264 @@
+/*
+ * 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 android.adservices.test.scenario.adservices.utils;
+
+import android.adservices.common.AdSelectionSignals;
+
+import com.google.common.collect.ImmutableList;
+import com.google.mockwebserver.Dispatcher;
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.RecordedRequest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/** Setup the dispatcher for mock web server. */
+public final class MockWebServerDispatcherFactory {
+
+ public static final String DECISION_LOGIC_PATH = "/seller/decision/simple_logic_with_delay";
+ public static final String TRUSTED_SCORING_SIGNAL_PATH =
+ "/trusted/scoringsignals/simple_with_delay";
+ public static final String TRUSTED_BIDDING_SIGNALS_PATH =
+ "/trusted/biddingsignals/simple_with_delay";
+ public static final ArrayList<String> VALID_TRUSTED_BIDDING_KEYS =
+ new ArrayList<>(Arrays.asList("example", "valid", "list", "of", "keys"));
+ // Estimated based on
+ // https://docs.google.com/spreadsheets/d/1EP_cwBbwYI-NMro0Qq5uif1krwjIQhjK8fjOu15j7hQ/edit?usp=sharing&resourcekey=0-A67kzEnAKKz1k7qpshSedg
+ public static final int SCORING_JS_EXECUTION_TIME_p50_MS = 40;
+ public static final int BIDDING_JS_EXECUTION_TIME_p50_MS = 40;
+ public static final int SCORING_JS_EXECUTION_TIME_p90_MS = 70;
+ public static final int BIDDING_JS_EXECUTION_TIME_p90_MS = 70;
+ public static final int DECISION_LOGIC_FETCH_DELAY_5G_p50_MS = 22;
+ public static final int DECISION_LOGIC_FETCH_DELAY_5G_p90_MS = 23;
+ public static final int DECISION_LOGIC_FETCH_DELAY_4GPLUS_p50_MS = 56;
+ public static final int DECISION_LOGIC_FETCH_DELAY_4GPLUS_p90_MS = 57;
+ public static final int DECISION_LOGIC_FETCH_DELAY_4G_p50_MS = 114;
+ public static final int DECISION_LOGIC_FETCH_DELAY_4G_p90_MS = 116;
+ public static final int BIDDING_LOGIC_FETCH_DELAY_5G_p50_MS = 23;
+ public static final int BIDDING_LOGIC_FETCH_DELAY_5G_p90_MS = 25;
+ public static final int BIDDING_LOGIC_FETCH_DELAY_4GPLUS_p50_MS = 57;
+ public static final int BIDDING_LOGIC_FETCH_DELAY_4GPLUS_p90_MS = 62;
+ public static final int BIDDING_LOGIC_FETCH_DELAY_4G_p50_MS = 116;
+ public static final int BIDDING_LOGIC_FETCH_DELAY_4G_p90_MS = 128;
+ public static final int SCORING_SIGNALS_FETCH_DELAY_5G_p50_MS = 21;
+ public static final int SCORING_SIGNALS_FETCH_DELAY_5G_p90_MS = 22;
+ public static final int SCORING_SIGNALS_FETCH_DELAY_4GPLUS_p50_MS = 51;
+ public static final int SCORING_SIGNALS_FETCH_DELAY_4GPLUS_p90_MS = 52;
+ public static final int SCORING_SIGNALS_FETCH_DELAY_4G_p50_MS = 101;
+ public static final int SCORING_SIGNALS_FETCH_DELAY_4G_p90_MS = 104;
+ public static final int BIDDING_SIGNALS_FETCH_DELAY_5G_p50_MS = 22;
+ public static final int BIDDING_SIGNALS_FETCH_DELAY_5G_p90_MS = 47;
+ public static final int BIDDING_SIGNALS_FETCH_DELAY_4GPLUS_p50_MS = 53;
+ public static final int BIDDING_SIGNALS_FETCH_DELAY_4GPLUS_p90_MS = 123;
+ public static final int BIDDING_SIGNALS_FETCH_DELAY_4G_p50_MS = 105;
+ public static final int BIDDING_SIGNALS_FETCH_DELAY_4G_p90_MS = 275;
+ private static final String BUYER_REPORTING_PATH = "/reporting/buyer";
+ private static final String SELLER_REPORTING_PATH = "/reporting/seller";
+ private static final String BUYER_BIDDING_LOGIC_URI_PATH =
+ "/buyer/bidding/simple_logic_with_delay";
+ private static final String DEFAULT_DECISION_LOGIC_JS_WITH_EXECUTION_TIME_FORMAT =
+ "function scoreAd(ad, bid, auction_config, seller_signals,"
+ + " trusted_scoring_signals, contextual_signal, user_signal,"
+ + " custom_audience_signal) { \n"
+ + " const start = Date.now(); let now = start; while (now-start < %d) "
+ + "{now=Date.now();}\n"
+ + " return {'status': 0, 'score': bid };\n"
+ + "}\n"
+ + "function reportResult(ad_selection_config, render_uri, bid,"
+ + " contextual_signals) { \n"
+ + " return {'status': 0, 'results': {'signals_for_buyer':"
+ + " '{\"signals_for_buyer\":1}', 'reporting_uri': '%s"
+ + "' } };\n"
+ + "}";
+ private static final String DEFAULT_BIDDING_LOGIC_JS_WITH_EXECUTION_TIME_FORMAT =
+ "function generateBid(ad, auction_signals, per_buyer_signals,"
+ + " trusted_bidding_signals, contextual_signals,"
+ + " custom_audience_signals) { \n"
+ + " const start = Date.now(); let now = start; while (now-start < %d) "
+ + "{now=Date.now();}\n"
+ + " return {'status': 0, 'ad': ad, 'bid': ad.metadata.result };\n"
+ + "}\n"
+ + "function reportWin(ad_selection_signals, per_buyer_signals,"
+ + " signals_for_buyer, contextual_signals, custom_audience_signals) { \n"
+ + " return {'status': 0, 'results': {'reporting_uri': '%s"
+ + "' } };\n"
+ + "}";
+ private static final AdSelectionSignals TRUSTED_SCORING_SIGNALS =
+ AdSelectionSignals.fromString(
+ "{\n"
+ + "\t\"render_uri_1\": \"signals_for_1\",\n"
+ + "\t\"render_uri_2\": \"signals_for_2\"\n"
+ + "}");
+ private static final AdSelectionSignals TRUSTED_BIDDING_SIGNALS =
+ AdSelectionSignals.fromString(
+ "{\n"
+ + "\t\"example\": \"example\",\n"
+ + "\t\"valid\": \"Also valid\",\n"
+ + "\t\"list\": \"list\",\n"
+ + "\t\"of\": \"of\",\n"
+ + "\t\"keys\": \"trusted bidding signal Values\"\n"
+ + "}");
+
+ public static Dispatcher create5Gp50LatencyDispatcher(MockWebServerRule mockWebServerRule) {
+ return create(
+ DECISION_LOGIC_FETCH_DELAY_5G_p50_MS,
+ BIDDING_LOGIC_FETCH_DELAY_5G_p50_MS,
+ SCORING_SIGNALS_FETCH_DELAY_5G_p50_MS,
+ BIDDING_SIGNALS_FETCH_DELAY_5G_p50_MS,
+ BIDDING_JS_EXECUTION_TIME_p50_MS,
+ SCORING_JS_EXECUTION_TIME_p50_MS,
+ mockWebServerRule);
+ }
+
+ public static Dispatcher create5Gp90LatencyDispatcher(MockWebServerRule mockWebServerRule) {
+ return create(
+ DECISION_LOGIC_FETCH_DELAY_5G_p90_MS,
+ BIDDING_LOGIC_FETCH_DELAY_5G_p90_MS,
+ SCORING_SIGNALS_FETCH_DELAY_5G_p90_MS,
+ BIDDING_SIGNALS_FETCH_DELAY_5G_p90_MS,
+ BIDDING_JS_EXECUTION_TIME_p90_MS,
+ SCORING_JS_EXECUTION_TIME_p90_MS,
+ mockWebServerRule);
+ }
+
+ public static Dispatcher create4GPlusp50LatencyDispatcher(MockWebServerRule mockWebServerRule) {
+ return create(
+ DECISION_LOGIC_FETCH_DELAY_4GPLUS_p50_MS,
+ BIDDING_LOGIC_FETCH_DELAY_4GPLUS_p50_MS,
+ SCORING_SIGNALS_FETCH_DELAY_4GPLUS_p50_MS,
+ BIDDING_SIGNALS_FETCH_DELAY_4GPLUS_p50_MS,
+ BIDDING_JS_EXECUTION_TIME_p50_MS,
+ SCORING_JS_EXECUTION_TIME_p50_MS,
+ mockWebServerRule);
+ }
+
+ public static Dispatcher create4GPlusp90LatencyDispatcher(MockWebServerRule mockWebServerRule) {
+ return create(
+ DECISION_LOGIC_FETCH_DELAY_4GPLUS_p90_MS,
+ BIDDING_LOGIC_FETCH_DELAY_4GPLUS_p90_MS,
+ SCORING_SIGNALS_FETCH_DELAY_4GPLUS_p90_MS,
+ BIDDING_SIGNALS_FETCH_DELAY_4GPLUS_p90_MS,
+ BIDDING_JS_EXECUTION_TIME_p90_MS,
+ SCORING_JS_EXECUTION_TIME_p90_MS,
+ mockWebServerRule);
+ }
+
+ public static Dispatcher create4Gp50LatencyDispatcher(MockWebServerRule mockWebServerRule) {
+ return create(
+ DECISION_LOGIC_FETCH_DELAY_4G_p50_MS,
+ BIDDING_LOGIC_FETCH_DELAY_4G_p50_MS,
+ SCORING_SIGNALS_FETCH_DELAY_4G_p50_MS,
+ BIDDING_SIGNALS_FETCH_DELAY_4G_p50_MS,
+ BIDDING_JS_EXECUTION_TIME_p50_MS,
+ SCORING_JS_EXECUTION_TIME_p50_MS,
+ mockWebServerRule);
+ }
+
+ public static Dispatcher create4Gp90LatencyDispatcher(MockWebServerRule mockWebServerRule) {
+ return create(
+ DECISION_LOGIC_FETCH_DELAY_4G_p90_MS,
+ BIDDING_LOGIC_FETCH_DELAY_4G_p90_MS,
+ SCORING_SIGNALS_FETCH_DELAY_4G_p90_MS,
+ BIDDING_SIGNALS_FETCH_DELAY_4G_p90_MS,
+ BIDDING_JS_EXECUTION_TIME_p90_MS,
+ SCORING_JS_EXECUTION_TIME_p90_MS,
+ mockWebServerRule);
+ }
+
+ private static Dispatcher create(
+ int decisionLogicFetchDelayMs,
+ int biddingLogicFetchDelayMs,
+ int scoringSignalFetchDelayMs,
+ int biddingSignalFetchDelayMs,
+ int biddingLogicExecutionRunMs,
+ int scoringLogicExecutionRunMs,
+ MockWebServerRule mockWebServerRule) {
+
+ return new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ if (DECISION_LOGIC_PATH.equals(request.getPath())) {
+ return new MockResponse()
+ .setBodyDelayTimeMs(decisionLogicFetchDelayMs)
+ .setBody(
+ getDecisionLogicJS(
+ scoringLogicExecutionRunMs,
+ mockWebServerRule
+ .uriForPath(SELLER_REPORTING_PATH)
+ .toString()));
+ } else if (BUYER_BIDDING_LOGIC_URI_PATH.equals(request.getPath())) {
+ return new MockResponse()
+ .setBodyDelayTimeMs(biddingLogicFetchDelayMs)
+ .setBody(
+ getBiddingLogicJS(
+ biddingLogicExecutionRunMs,
+ mockWebServerRule
+ .uriForPath(BUYER_REPORTING_PATH)
+ .toString()));
+ } else if (BUYER_REPORTING_PATH.equals(request.getPath())
+ || SELLER_REPORTING_PATH.equals(request.getPath())) {
+ return new MockResponse().setBody("");
+ } else if (request.getPath().startsWith(TRUSTED_SCORING_SIGNAL_PATH)) {
+ return new MockResponse()
+ .setBodyDelayTimeMs(scoringSignalFetchDelayMs)
+ .setBody(TRUSTED_SCORING_SIGNALS.toString());
+ } else if (request.getPath().startsWith(TRUSTED_BIDDING_SIGNALS_PATH)) {
+ return new MockResponse()
+ .setBodyDelayTimeMs(biddingSignalFetchDelayMs)
+ .setBody(TRUSTED_BIDDING_SIGNALS.toString());
+ }
+ return new MockResponse().setResponseCode(404);
+ }
+ };
+ }
+
+ public static String getBiddingLogicUriPath() {
+ return BUYER_BIDDING_LOGIC_URI_PATH;
+ }
+
+ public static String getDecisionLogicPath() {
+ return DECISION_LOGIC_PATH;
+ }
+
+ public static String getTrustedScoringSignalPath() {
+ return TRUSTED_SCORING_SIGNAL_PATH;
+ }
+
+ public static ImmutableList<String> getValidTrustedBiddingKeys() {
+ return ImmutableList.copyOf(VALID_TRUSTED_BIDDING_KEYS);
+ }
+
+ public static String getTrustedBiddingSignalsPath() {
+ return TRUSTED_BIDDING_SIGNALS_PATH;
+ }
+
+ private static String getDecisionLogicJS(
+ int scoringLogicExecutionRunMs, String sellerReportingUri) {
+ return String.format(
+ DEFAULT_DECISION_LOGIC_JS_WITH_EXECUTION_TIME_FORMAT,
+ scoringLogicExecutionRunMs,
+ sellerReportingUri);
+ }
+
+ private static String getBiddingLogicJS(
+ int biddingLogicExecutionRunMs, String buyerReportingUri) {
+ return String.format(
+ DEFAULT_BIDDING_LOGIC_JS_WITH_EXECUTION_TIME_FORMAT,
+ biddingLogicExecutionRunMs,
+ buyerReportingUri);
+ }
+}
diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRule.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRule.java
new file mode 100644
index 0000000000..ecc447977f
--- /dev/null
+++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRule.java
@@ -0,0 +1,289 @@
+/*
+ * 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 android.adservices.test.scenario.adservices.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.google.mockwebserver.Dispatcher;
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockWebServer;
+import com.google.mockwebserver.RecordedRequest;
+
+import org.junit.Assert;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ServerSocket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+/** Instances of this class are not thread safe. */
+public class MockWebServerRule implements TestRule {
+ private static final int UNINITIALIZED = -1;
+ private final InputStream mCertificateInputStream;
+ private final char[] mKeyStorePassword;
+ private int mPort = UNINITIALIZED;
+ private MockWebServer mMockWebServer;
+
+ private MockWebServerRule(InputStream inputStream, String keyStorePassword) {
+ mCertificateInputStream = inputStream;
+ mKeyStorePassword = keyStorePassword == null ? null : keyStorePassword.toCharArray();
+ }
+
+ public static MockWebServerRule forHttp() {
+ return new MockWebServerRule(null, null);
+ }
+
+ /**
+ * Builds an instance of the MockWebServerRule configured for HTTPS traffic.
+ *
+ * @param context The app context used to load the PKCS12 key store
+ * @param assetName The name of the key store under the app assets folder
+ * @param keyStorePassword The password of the keystore
+ */
+ public static MockWebServerRule forHttps(
+ Context context, String assetName, String keyStorePassword) {
+ try {
+ return new MockWebServerRule(context.getAssets().open(assetName), keyStorePassword);
+ } catch (IOException ioException) {
+ throw new RuntimeException("Unable to initialize MockWebServerRule", ioException);
+ }
+ }
+
+ /**
+ * Builds an instance of the MockWebServerRule configured for HTTPS traffic.
+ *
+ * @param certificateInputStream An input stream to load the content of a PKCS12 key store
+ * @param keyStorePassword The password of the keystore
+ */
+ public static MockWebServerRule forHttps(
+ InputStream certificateInputStream, String keyStorePassword) {
+ return new MockWebServerRule(certificateInputStream, keyStorePassword);
+ }
+
+ private boolean useHttps() {
+ return Objects.nonNull(mCertificateInputStream);
+ }
+
+ public MockWebServer startMockWebServer(List<MockResponse> responses) throws Exception {
+ if (mPort == UNINITIALIZED) {
+ reserveServerListeningPort();
+ }
+
+ mMockWebServer = new MockWebServer();
+ if (useHttps()) {
+ mMockWebServer.useHttps(getTestingSslSocketFactory(), false);
+ }
+ for (MockResponse response : responses) {
+ mMockWebServer.enqueue(response);
+ }
+ mMockWebServer.play(mPort);
+ return mMockWebServer;
+ }
+
+ public MockWebServer startMockWebServer(Function<RecordedRequest, MockResponse> lambda)
+ throws Exception {
+ Dispatcher dispatcher =
+ new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) {
+ return lambda.apply(request);
+ }
+ };
+ return startMockWebServer(dispatcher);
+ }
+
+ public MockWebServer startMockWebServer(Dispatcher dispatcher) throws Exception {
+ if (mPort == UNINITIALIZED) {
+ reserveServerListeningPort();
+ }
+
+ mMockWebServer = new MockWebServer();
+ if (useHttps()) {
+ mMockWebServer.useHttps(getTestingSslSocketFactory(), false);
+ }
+ mMockWebServer.setDispatcher(dispatcher);
+
+ mMockWebServer.play(mPort);
+ return mMockWebServer;
+ }
+
+ public MockWebServer createMockWebServer() throws Exception {
+ if (mPort == UNINITIALIZED) {
+ reserveServerListeningPort();
+ }
+
+ mMockWebServer = new MockWebServer();
+ if (useHttps()) {
+ mMockWebServer.useHttps(getTestingSslSocketFactory(), false);
+ }
+ return mMockWebServer;
+ }
+
+ public MockWebServer startCreatedMockWebServer(Dispatcher dispatcher) throws Exception {
+ if (mMockWebServer == null || mPort == UNINITIALIZED) {
+ throw new IllegalStateException(
+ "MockWebServer is not created or the port is not reserved.");
+ }
+ mMockWebServer.setDispatcher(dispatcher);
+
+ mMockWebServer.play(mPort);
+ return mMockWebServer;
+ }
+
+ /**
+ * @return the mock web server for this rull and {@code null} if it hasn't been started yet by
+ * calling {@link #startMockWebServer(List)}.
+ */
+ public MockWebServer getMockWebServer() {
+ return mMockWebServer;
+ }
+
+ /** @return the base address the mock web server will be listening to when started. */
+ public String getServerBaseAddress() {
+ return String.format("%s://localhost:%d", useHttps() ? "https" : "http", mPort);
+ }
+
+ /**
+ * This method is equivalent to {@link MockWebServer#getUrl(String)} but it can be used before
+ * you prepare and start the server if you need to prepare responses that will reference the
+ * same test server.
+ *
+ * @return an Uri to use to reach the given {@code @path} on the mock web server.
+ */
+ public Uri uriForPath(String path) {
+ return Uri.parse(
+ String.format(
+ "%s%s%s", getServerBaseAddress(), path.startsWith("/") ? "" : "/", path));
+ }
+
+ private void reserveServerListeningPort() throws IOException {
+ ServerSocket serverSocket = new ServerSocket(38383);
+ serverSocket.setReuseAddress(true);
+ mPort = serverSocket.getLocalPort();
+ serverSocket.close();
+ }
+
+ private SSLSocketFactory getTestingSslSocketFactory()
+ throws GeneralSecurityException, IOException {
+ final KeyManagerFactory keyManagerFactory =
+ KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ KeyStore keyStore = KeyStore.getInstance("PKCS12");
+ keyStore.load(mCertificateInputStream, mKeyStorePassword);
+ keyManagerFactory.init(keyStore, mKeyStorePassword);
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
+ return sslContext.getSocketFactory();
+ }
+
+ /**
+ * A utility that validates that the mock web server got the expected traffic.
+ *
+ * @param mockWebServer server instance used for making requests
+ * @param expectedRequestCount the number of requests expected to be received by the server
+ * @param expectedRequests the list of URLs that should have been requested, in case of repeat
+ * requests the size of expectedRequests list could be less than the expectedRequestCount
+ * @param requestMatcher A custom matcher that dictates if the request meets the criteria of
+ * being hit or not. This allows tests to do partial match of URLs in case of params or
+ * other sub path of URL.
+ */
+ public void verifyMockServerRequests(
+ final MockWebServer mockWebServer,
+ final int expectedRequestCount,
+ final List<String> expectedRequests,
+ final RequestMatcher<String> requestMatcher) {
+
+ assertEquals(
+ "Number of expected requests does not match actual request count",
+ expectedRequestCount,
+ mockWebServer.getRequestCount());
+
+ // For parallel executions requests should be checked agnostic of order
+ final Set<String> actualRequests = new HashSet<>();
+ for (int i = 0; i < expectedRequestCount; i++) {
+ try {
+ actualRequests.add(mockWebServer.takeRequest().getPath());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ assertFalse(
+ String.format(
+ "Expected requests cannot be empty, actual requests <%s>", actualRequests),
+ expectedRequestCount != 0 && expectedRequests.isEmpty());
+
+ for (String request : expectedRequests) {
+ Assert.assertTrue(
+ String.format(
+ "Actual requests <%s> do not contain request <%s>",
+ actualRequests, request),
+ wasPathRequested(actualRequests, request, requestMatcher));
+ }
+ }
+
+ private boolean wasPathRequested(
+ final Set<String> actualRequests,
+ final String request,
+ final RequestMatcher<String> requestMatcher) {
+ for (String actualRequest : actualRequests) {
+ // Passing a custom comparator allows tests to do partial match of URLs in case of
+ // params or other sub path of URL
+ if (requestMatcher.wasRequestMade(actualRequest, request)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ reserveServerListeningPort();
+ try {
+ base.evaluate();
+ } finally {
+ if (mMockWebServer != null) {
+ mMockWebServer.shutdown();
+ }
+ }
+ }
+ };
+ }
+
+ public interface RequestMatcher<T> {
+ boolean wasRequestMade(T actualRequest, T expectedRequest);
+ }
+}
diff --git a/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRuleFactory.java b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRuleFactory.java
new file mode 100644
index 0000000000..00f3143633
--- /dev/null
+++ b/adservices/tests/perf/src/android/adservices/test/scenario/adservices/utils/MockWebServerRuleFactory.java
@@ -0,0 +1,35 @@
+/*
+ * 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 android.adservices.test.scenario.adservices.utils;
+
+import androidx.test.core.app.ApplicationProvider;
+
+/** Utility class for tests needing to mock web server calls */
+public class MockWebServerRuleFactory {
+ /** @return A mock {@link MockWebServerRule} initialized to use HTTPS. */
+ public static MockWebServerRule createForHttps() {
+ return MockWebServerRule.forHttps(
+ ApplicationProvider.getApplicationContext(),
+ "adservices_test_server.p12",
+ "adservices_test");
+ }
+
+ /** @return A mock {@link MockWebServerRule} initialized to use HTTP cleartext. */
+ public static MockWebServerRule createForHttp() {
+ return MockWebServerRule.forHttp();
+ }
+}