• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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