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.topics; 18 19 import static com.android.adservices.service.FlagsConstants.KEY_CLASSIFIER_FORCE_USE_BUNDLED_FILES; 20 21 import static com.google.common.truth.Truth.assertThat; 22 23 import android.adservices.clients.topics.AdvertisingTopicsClient; 24 import android.adservices.topics.GetTopicsResponse; 25 import android.adservices.topics.Topic; 26 import android.content.Context; 27 import android.platform.test.scenario.annotation.Scenario; 28 import android.util.Log; 29 30 import androidx.test.core.app.ApplicationProvider; 31 32 import com.android.adservices.common.AdServicesFlagsSetterRule; 33 import com.android.adservices.common.AdservicesTestHelper; 34 import com.android.compatibility.common.util.ShellUtils; 35 36 import org.junit.Before; 37 import org.junit.Rule; 38 import org.junit.Test; 39 import org.junit.runner.RunWith; 40 import org.junit.runners.JUnit4; 41 42 import java.io.BufferedReader; 43 import java.io.IOException; 44 import java.io.InputStream; 45 import java.io.InputStreamReader; 46 import java.text.ParseException; 47 import java.text.SimpleDateFormat; 48 import java.time.Clock; 49 import java.time.Instant; 50 import java.time.ZoneId; 51 import java.time.format.DateTimeFormatter; 52 import java.util.Arrays; 53 import java.util.Date; 54 import java.util.HashMap; 55 import java.util.Map; 56 import java.util.concurrent.Executor; 57 import java.util.concurrent.Executors; 58 59 /** Crystalball test for Topics API to test epoch computation using the Precomputed classifier. */ 60 @Scenario 61 @RunWith(JUnit4.class) 62 public class TopicsEpochComputationPrecomputedClassifier { 63 private static final String TAG = "TopicsEpochComputation"; 64 65 // Metric name for Crystalball test 66 private static final String EPOCH_COMPUTATION_DURATION = "EPOCH_COMPUTATION_DURATION"; 67 68 // The JobId of the Epoch Computation. 69 private static final int EPOCH_JOB_ID = 2; 70 71 // Override the Epoch Job Period to this value to speed up the epoch computation. 72 private static final long TEST_EPOCH_JOB_PERIOD_MS = 3000; 73 74 // Default Epoch Period. 75 private static final long TOPICS_EPOCH_JOB_PERIOD_MS = 7 * 86_400_000; // 7 days. 76 77 // Use 0 percent for random topic in the test so that we can verify the returned topic. 78 private static final int TEST_TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC = 0; 79 private static final int TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC = 5; 80 81 protected static final Context sContext = ApplicationProvider.getApplicationContext(); 82 private static final Executor CALLBACK_EXECUTOR = Executors.newCachedThreadPool(); 83 84 private static final String ADSERVICES_PACKAGE_NAME = 85 AdservicesTestHelper.getAdServicesPackageName(sContext, TAG); 86 87 private static final DateTimeFormatter LOG_TIME_FORMATTER = 88 DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); 89 90 private static final String EPOCH_COMPUTATION_START_LOG = "Start of Epoch Computation"; 91 92 private static final String EPOCH_COMPUTATION_END_LOG = "End of Epoch Computation"; 93 94 private static final String EPOCH_START_TIMESTAMP_KEY = "start"; 95 96 private static final String EPOCH_STOP_TIMESTAMP_KEY = "end"; 97 98 private static final EpochSleeper sSleeper = 99 EpochSleeper.getInstance(sContext, TEST_EPOCH_JOB_PERIOD_MS); 100 101 @Rule 102 public final AdServicesFlagsSetterRule flags = 103 AdServicesFlagsSetterRule.forTopicsPerfTests( 104 TEST_EPOCH_JOB_PERIOD_MS, TEST_TOPICS_PERCENTAGE_FOR_RANDOM_TOPIC) 105 // Turn off MDD to avoid model mismatching 106 .setMddBackgroundTaskKillSwitch(true) 107 .setFlag(KEY_CLASSIFIER_FORCE_USE_BUNDLED_FILES, true); 108 109 @Before setup()110 public void setup() throws Exception { 111 sSleeper.triggerOriginLog(); 112 113 // We need to skip 3 epochs so that if there is any usage from other test runs, it will 114 // not be used for epoch retrieval. 115 Thread.sleep(3 * TEST_EPOCH_JOB_PERIOD_MS); 116 sSleeper.sleepUntilNextEpoch(); 117 } 118 119 120 @Test testEpochComputation()121 public void testEpochComputation() throws Exception { 122 // The Test App has 2 SDKs: sdk1 calls the Topics API and sdk2 does not. 123 // Sdk1 calls the Topics API. 124 AdvertisingTopicsClient advertisingTopicsClient1 = 125 new AdvertisingTopicsClient.Builder() 126 .setContext(sContext) 127 .setSdkName("sdk1") 128 .setExecutor(CALLBACK_EXECUTOR) 129 .build(); 130 131 // At beginning, Sdk1 receives no topic. 132 GetTopicsResponse sdk1Result = advertisingTopicsClient1.getTopics().get(); 133 assertThat(sdk1Result.getTopics()).isEmpty(); 134 135 Instant startTime = Clock.systemUTC().instant(); 136 // Now force the Epoch Computation Job. This should be done in the same epoch for 137 // callersCanLearnMap to have the entry for processing. 138 forceEpochComputationJob(); 139 140 // Wait to the next epoch. We will not need to do this after we implement the fix in 141 // go/rb-topics-epoch-scheduling 142 sSleeper.sleepUntilNextEpoch(); 143 144 // calculate and log epoch computation duration after some delay so that epoch 145 // computation job is finished. 146 logEpochComputationDuration(startTime); 147 148 // Since the sdk1 called the Topics API in the previous Epoch, it should receive some topic. 149 sdk1Result = advertisingTopicsClient1.getTopics().get(); 150 assertThat(sdk1Result.getTopics()).isNotEmpty(); 151 152 // We only have 1 test app which has 5 classification topics: 10147,10253,10175,10254,10333 153 // in the precomputed list. 154 // These 5 classification topics will become top 5 topics of the epoch since there is 155 // no other apps calling Topics API. 156 // The app will be assigned one random topic from one of these 5 topics. 157 assertThat(sdk1Result.getTopics()).hasSize(1); 158 Topic topic = sdk1Result.getTopics().get(0); 159 160 // topic is one of the 5 classification topics of the Test App. 161 assertThat(topic.getTopicId()).isIn(Arrays.asList(10147, 10253, 10175, 10254, 10333)); 162 163 assertThat(topic.getModelVersion()).isAtLeast(1L); 164 assertThat(topic.getTaxonomyVersion()).isAtLeast(1L); 165 166 // Sdk 2 did not call getTopics API. So it should not receive any topic. 167 AdvertisingTopicsClient advertisingTopicsClient2 = 168 new AdvertisingTopicsClient.Builder() 169 .setContext(sContext) 170 .setSdkName("sdk2") 171 .setExecutor(CALLBACK_EXECUTOR) 172 .build(); 173 174 GetTopicsResponse sdk2Result2 = advertisingTopicsClient2.getTopics().get(); 175 assertThat(sdk2Result2.getTopics()).isEmpty(); 176 } 177 178 /** Forces JobScheduler to run the Epoch Computation job */ forceEpochComputationJob()179 private void forceEpochComputationJob() { 180 ShellUtils.runShellCommand( 181 "cmd jobscheduler run -f" + " " + ADSERVICES_PACKAGE_NAME + " " + EPOCH_JOB_ID); 182 } 183 logEpochComputationDuration(Instant startTime)184 private void logEpochComputationDuration(Instant startTime) throws Exception { 185 long epoch_computation_duration = 186 processLogCatStreamToGetMetricMap(getMetricsEvents(startTime)); 187 Log.i(TAG, "(" + EPOCH_COMPUTATION_DURATION + ": " + epoch_computation_duration + ")"); 188 } 189 190 /** Return AdServices(EpochManager) logs that will be used to build the test metrics. */ getMetricsEvents(Instant startTime)191 public InputStream getMetricsEvents(Instant startTime) throws IOException { 192 ProcessBuilder pb = 193 new ProcessBuilder( 194 Arrays.asList( 195 "logcat", 196 "-s", 197 "adservices.topics:V", 198 "-t", 199 LOG_TIME_FORMATTER.format(startTime), 200 "|", 201 "grep", 202 "Epoch")); 203 return pb.start().getInputStream(); 204 } 205 206 /** 207 * Filters the start and end log for the epoch computation and based on that calculates the 208 * duration of epoch computation. If we fail to parse the start or end log for epoch 209 * computation, we catch ParseException and in the end throw an exception. 210 * 211 * @param inputStream the logcat stream which contains start and end time info for the epoch 212 * computation 213 * @return the value of epoch computation latency 214 * @throws Exception if the test failed to get the time point for epoch computation's start and 215 * end. 216 */ processLogCatStreamToGetMetricMap(InputStream inputStream)217 private Long processLogCatStreamToGetMetricMap(InputStream inputStream) throws Exception { 218 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); 219 Map<String, Long> output = new HashMap<String, Long>(); 220 bufferedReader 221 .lines() 222 .filter( 223 line -> 224 line.contains(EPOCH_COMPUTATION_START_LOG) 225 || line.contains(EPOCH_COMPUTATION_END_LOG)) 226 .forEach( 227 line -> { 228 if (line.contains(EPOCH_COMPUTATION_START_LOG)) { 229 try { 230 output.put( 231 EPOCH_START_TIMESTAMP_KEY, getTimestampFromLog(line)); 232 } catch (ParseException e) { 233 Log.e( 234 TAG, 235 String.format( 236 "Caught ParseException when fetching start" 237 + " time for epoch computation: %s", 238 e.toString())); 239 } 240 } else { 241 try { 242 output.put(EPOCH_STOP_TIMESTAMP_KEY, getTimestampFromLog(line)); 243 } catch (ParseException e) { 244 Log.e( 245 TAG, 246 String.format( 247 "Caught ParseException when fetching end time" 248 + " for epoch computation: %s", 249 e.toString())); 250 } 251 } 252 }); 253 254 if (output.containsKey(EPOCH_START_TIMESTAMP_KEY) 255 && output.containsKey(EPOCH_STOP_TIMESTAMP_KEY)) { 256 return output.get(EPOCH_STOP_TIMESTAMP_KEY) - output.get(EPOCH_START_TIMESTAMP_KEY); 257 } 258 throw new Exception("Cannot get the time of Epoch Computation's start and end"); 259 } 260 261 /** 262 * Parses the timestamp from the log. Example log: 10-06 17:58:20.173 14950 14966 D adservices: 263 * Start of Epoch Computation 264 */ getTimestampFromLog(String log)265 private static Long getTimestampFromLog(String log) throws ParseException { 266 String[] words = log.split(" "); 267 SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd hh:mm:ss.SSS"); 268 Date parsedDate = dateFormat.parse(words[0] + " " + words[1]); 269 return parsedDate.getTime(); 270 } 271 } 272