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 com.android.car.internal.property; 18 19 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO; 20 import static com.android.car.internal.property.CarPropertyHelper.propertyIdsToString; 21 import static com.android.car.internal.util.ArrayUtils.convertToIntArray; 22 import static com.android.car.internal.util.DebugUtils.toAreaIdString; 23 24 import android.annotation.Nullable; 25 import android.car.VehiclePropertyIds; 26 import android.util.ArrayMap; 27 import android.util.ArraySet; 28 import android.util.Log; 29 import android.util.Slog; 30 31 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport; 32 import com.android.car.internal.util.IndentingPrintWriter; 33 import com.android.car.internal.util.PairSparseArray; 34 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.Objects; 38 import java.util.Set; 39 import java.util.TreeSet; 40 41 /** 42 * This class manages [{propertyId, areaId} -> RateInfoForClients] map and maintains two states: 43 * a current state and a staged state. The staged state represents the proposed changes. After 44 * the changes are applied to the lower layer, caller either uses {@link #commit} to replace 45 * the curren state with the staged state, or uses {@link #dropCommit} to drop the staged state. 46 * 47 * A common pattern is 48 * 49 * ``` 50 * synchronized (mLock) { 51 * mSubscriptionManager.stageNewOptions(...); 52 * // Optionally stage some other options. 53 * mSubscriptionManager.stageNewOptions(...); 54 * // Optionally stage unregistration. 55 * mSubscriptionManager.stageUnregister(...); 56 * 57 * mSubscriptionManager.diffBetweenCurrentAndStage(...); 58 * try { 59 * // Apply the diff. 60 * } catch (Exception e) { 61 * mSubscriptionManager.dropCommit(); 62 * throw e; 63 * } 64 * mSubscriptionManager.commit(); 65 * } 66 * ``` 67 * 68 * This class is not thread-safe. 69 * 70 * @param <ClientType> A class representing a client. 71 */ 72 public final class SubscriptionManager<ClientType> { 73 private static final String TAG = SubscriptionManager.class.getSimpleName(); 74 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 75 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); 76 77 private static final class RateInfo { 78 public final float updateRateHz; 79 public final boolean enableVariableUpdateRate; 80 public final float resolution; 81 RateInfo(float updateRateHz, boolean enableVariableUpdateRate, float resolution)82 RateInfo(float updateRateHz, boolean enableVariableUpdateRate, float resolution) { 83 this.updateRateHz = updateRateHz; 84 this.enableVariableUpdateRate = enableVariableUpdateRate; 85 this.resolution = resolution; 86 } 87 88 @Override toString()89 public String toString() { 90 return String.format( 91 "RateInfo{updateRateHz: %f, enableVur: %b, resolution: %f}", updateRateHz, 92 enableVariableUpdateRate, resolution); 93 } 94 95 @Override equals(Object other)96 public boolean equals(Object other) { 97 if (this == other) { 98 return true; 99 } 100 if (!(other instanceof RateInfo)) { 101 return false; 102 } 103 RateInfo that = (RateInfo) other; 104 return updateRateHz == that.updateRateHz 105 && enableVariableUpdateRate == that.enableVariableUpdateRate 106 && resolution == that.resolution; 107 } 108 109 @Override hashCode()110 public int hashCode() { 111 return Objects.hash(updateRateHz, enableVariableUpdateRate, resolution); 112 } 113 } 114 115 /** 116 * This class provides an abstraction for all the clients and their subscribed rate for a 117 * specific {propertyId, areaId} pair. 118 */ 119 private static final class RateInfoForClients<ClientType> { 120 private final ArrayMap<ClientType, RateInfo> mRateInfoByClient; 121 // An ordered set for all update rates to provide efficient add, remove and get max update 122 // rate. 123 private final TreeSet<Float> mUpdateRatesHz; 124 private final ArrayMap<Float, Integer> mClientCountByUpdateRateHz; 125 private final TreeSet<Float> mResolutions; 126 private final ArrayMap<Float, Integer> mClientCountByResolution; 127 // How many clients has enabled variable update rate. We can only enable variable update 128 // rate in the underlying layer if all clients enable VUR. 129 private int mEnableVariableUpdateRateCount; 130 RateInfoForClients()131 RateInfoForClients() { 132 mRateInfoByClient = new ArrayMap<>(); 133 mUpdateRatesHz = new TreeSet<>(); 134 mClientCountByUpdateRateHz = new ArrayMap<>(); 135 mResolutions = new TreeSet<>(); 136 mClientCountByResolution = new ArrayMap<>(); 137 } 138 RateInfoForClients(RateInfoForClients other)139 RateInfoForClients(RateInfoForClients other) { 140 mRateInfoByClient = new ArrayMap<>(other.mRateInfoByClient); 141 mUpdateRatesHz = new TreeSet<>(other.mUpdateRatesHz); 142 mClientCountByUpdateRateHz = new ArrayMap<>(other.mClientCountByUpdateRateHz); 143 mResolutions = new TreeSet<>(other.mResolutions); 144 mClientCountByResolution = new ArrayMap<>(other.mClientCountByResolution); 145 mEnableVariableUpdateRateCount = other.mEnableVariableUpdateRateCount; 146 } 147 148 /** 149 * Gets the max update rate for this {propertyId, areaId}. 150 */ getMaxUpdateRateHz()151 private float getMaxUpdateRateHz() { 152 return mUpdateRatesHz.last(); 153 } 154 155 /** 156 * Gets the min required resolution for this {propertyId, areaId}. 157 */ getMinRequiredResolution()158 private float getMinRequiredResolution() { 159 return mResolutions.first(); 160 } 161 isVariableUpdateRateEnabledForAllClients()162 private boolean isVariableUpdateRateEnabledForAllClients() { 163 return mEnableVariableUpdateRateCount == mRateInfoByClient.size(); 164 } 165 166 /** 167 * Gets the combined rate info for all clients. 168 * 169 * We use the max update rate, min required resolution, and only enable VUR if all clients 170 * enable. 171 */ getCombinedRateInfo()172 RateInfo getCombinedRateInfo() { 173 return new RateInfo(getMaxUpdateRateHz(), isVariableUpdateRateEnabledForAllClients(), 174 getMinRequiredResolution()); 175 } 176 getClients()177 Set<ClientType> getClients() { 178 return mRateInfoByClient.keySet(); 179 } 180 getUpdateRateHz(ClientType client)181 float getUpdateRateHz(ClientType client) { 182 return mRateInfoByClient.get(client).updateRateHz; 183 } 184 isVariableUpdateRateEnabled(ClientType client)185 boolean isVariableUpdateRateEnabled(ClientType client) { 186 return mRateInfoByClient.get(client).enableVariableUpdateRate; 187 } 188 getResolution(ClientType client)189 float getResolution(ClientType client) { 190 return mRateInfoByClient.get(client).resolution; 191 } 192 193 /** 194 * Adds a new client for this {propertyId, areaId}. 195 */ add(ClientType client, float updateRateHz, boolean enableVariableUpdateRate, float resolution)196 void add(ClientType client, float updateRateHz, boolean enableVariableUpdateRate, 197 float resolution) { 198 // Clear the existing updateRateHz for the client if exists. 199 remove(client); 200 // Store the new rate info. 201 mRateInfoByClient.put(client, 202 new RateInfo(updateRateHz, enableVariableUpdateRate, resolution)); 203 204 if (enableVariableUpdateRate) { 205 mEnableVariableUpdateRateCount++; 206 } 207 208 if (!mClientCountByUpdateRateHz.containsKey(updateRateHz)) { 209 mUpdateRatesHz.add(updateRateHz); 210 mClientCountByUpdateRateHz.put(updateRateHz, 1); 211 } else { 212 mClientCountByUpdateRateHz.put(updateRateHz, 213 mClientCountByUpdateRateHz.get(updateRateHz) + 1); 214 } 215 216 if (!mClientCountByResolution.containsKey(resolution)) { 217 mResolutions.add(resolution); 218 mClientCountByResolution.put(resolution, 1); 219 } else { 220 mClientCountByResolution.put(resolution, 221 mClientCountByResolution.get(resolution) + 1); 222 } 223 } 224 remove(ClientType client)225 void remove(ClientType client) { 226 if (!mRateInfoByClient.containsKey(client)) { 227 return; 228 } 229 RateInfo rateInfo = mRateInfoByClient.get(client); 230 if (rateInfo.enableVariableUpdateRate) { 231 mEnableVariableUpdateRateCount--; 232 } 233 float updateRateHz = rateInfo.updateRateHz; 234 if (mClientCountByUpdateRateHz.containsKey(updateRateHz)) { 235 int newCount = mClientCountByUpdateRateHz.get(updateRateHz) - 1; 236 if (newCount == 0) { 237 mClientCountByUpdateRateHz.remove(updateRateHz); 238 mUpdateRatesHz.remove(updateRateHz); 239 } else { 240 mClientCountByUpdateRateHz.put(updateRateHz, newCount); 241 } 242 } 243 float resolution = rateInfo.resolution; 244 if (mClientCountByResolution.containsKey(resolution)) { 245 int newCount = mClientCountByResolution.get(resolution) - 1; 246 if (newCount == 0) { 247 mClientCountByResolution.remove(resolution); 248 mResolutions.remove(resolution); 249 } else { 250 mClientCountByResolution.put(resolution, newCount); 251 } 252 } 253 254 mRateInfoByClient.remove(client); 255 } 256 isEmpty()257 boolean isEmpty() { 258 return mRateInfoByClient.isEmpty(); 259 } 260 } 261 262 PairSparseArray<RateInfoForClients<ClientType>> mCurrentRateInfoByClientByPropIdAreaId = 263 new PairSparseArray<>(); 264 PairSparseArray<RateInfoForClients<ClientType>> mStagedRateInfoByClientByPropIdAreaId = 265 new PairSparseArray<>(); 266 ArraySet<int[]> mStagedAffectedPropIdAreaIds = new ArraySet<>(); 267 268 /** 269 * Prepares new subscriptions. 270 * 271 * This apply the new subscribe options in the staging area without actually committing them. 272 * Client should call {@link #diffBetweenCurrentAndStage} to get the difference between current 273 * and the staging state. Apply them to the lower layer, and either commit the change after 274 * the operation succeeds or drop the change after the operation failed. 275 */ stageNewOptions(ClientType client, List<CarSubscription> options)276 public void stageNewOptions(ClientType client, List<CarSubscription> options) { 277 if (DBG) { 278 Slog.d(TAG, "stageNewOptions: options: " + options); 279 } 280 281 cloneCurrentToStageIfClean(); 282 283 for (int i = 0; i < options.size(); i++) { 284 CarSubscription option = options.get(i); 285 int propertyId = option.propertyId; 286 for (int areaId : option.areaIds) { 287 mStagedAffectedPropIdAreaIds.add(new int[]{propertyId, areaId}); 288 if (mStagedRateInfoByClientByPropIdAreaId.get(propertyId, areaId) == null) { 289 mStagedRateInfoByClientByPropIdAreaId.put(propertyId, areaId, 290 new RateInfoForClients<>()); 291 } 292 mStagedRateInfoByClientByPropIdAreaId.get(propertyId, areaId).add( 293 client, option.updateRateHz, option.enableVariableUpdateRate, 294 option.resolution); 295 } 296 } 297 } 298 299 /** 300 * Prepares unregistration for list of property IDs. 301 * 302 * This apply the unregistration in the staging area without actually committing them. 303 */ stageUnregister(ClientType client, ArraySet<Integer> propertyIdsToUnregister)304 public void stageUnregister(ClientType client, ArraySet<Integer> propertyIdsToUnregister) { 305 if (DBG) { 306 Slog.d(TAG, "stageUnregister: propertyIdsToUnregister: " + propertyIdsToString( 307 propertyIdsToUnregister)); 308 } 309 310 cloneCurrentToStageIfClean(); 311 312 for (int i = 0; i < propertyIdsToUnregister.size(); i++) { 313 int propertyId = propertyIdsToUnregister.valueAt(i); 314 ArraySet<Integer> areaIds = 315 mStagedRateInfoByClientByPropIdAreaId.getSecondKeysForFirstKey(propertyId); 316 for (int j = 0; j < areaIds.size(); j++) { 317 int areaId = areaIds.valueAt(j); 318 mStagedAffectedPropIdAreaIds.add(new int[]{propertyId, areaId}); 319 RateInfoForClients<ClientType> rateInfoForClients = 320 mStagedRateInfoByClientByPropIdAreaId.get(propertyId, areaId); 321 if (rateInfoForClients == null) { 322 Slog.e(TAG, "The property: " + VehiclePropertyIds.toString(propertyId) 323 + ", area ID: " + toAreaIdString(propertyId, areaId) 324 + " was not registered, do nothing"); 325 continue; 326 } 327 rateInfoForClients.remove(client); 328 if (rateInfoForClients.isEmpty()) { 329 mStagedRateInfoByClientByPropIdAreaId.remove(propertyId, areaId); 330 } 331 } 332 } 333 } 334 335 /** 336 * Commit the staged changes. 337 * 338 * This will replace the current state with the staged state. This should be called after the 339 * changes are applied successfully to the lower layer. 340 */ commit()341 public void commit() { 342 if (mStagedAffectedPropIdAreaIds.isEmpty()) { 343 if (VERBOSE) { 344 Slog.v(TAG, "No changes has been staged, nothing to commit"); 345 } 346 return; 347 } 348 // Drop the current state. 349 mCurrentRateInfoByClientByPropIdAreaId = mStagedRateInfoByClientByPropIdAreaId; 350 mStagedAffectedPropIdAreaIds.clear(); 351 } 352 353 /** 354 * Drop the staged changes. 355 * 356 * This should be called after the changes failed to apply to the lower layer. 357 */ dropCommit()358 public void dropCommit() { 359 if (mStagedAffectedPropIdAreaIds.isEmpty()) { 360 if (DBG) { 361 Slog.d(TAG, "No changes has been staged, nothing to drop"); 362 } 363 return; 364 } 365 // Drop the staged state. 366 mStagedRateInfoByClientByPropIdAreaId = mCurrentRateInfoByClientByPropIdAreaId; 367 mStagedAffectedPropIdAreaIds.clear(); 368 } 369 getCurrentSubscribedPropIds()370 public ArraySet<Integer> getCurrentSubscribedPropIds() { 371 return new ArraySet<Integer>(mCurrentRateInfoByClientByPropIdAreaId.getFirstKeys()); 372 } 373 374 /** 375 * Clear both the current state and staged state. 376 */ clear()377 public void clear() { 378 mStagedRateInfoByClientByPropIdAreaId.clear(); 379 mCurrentRateInfoByClientByPropIdAreaId.clear(); 380 mStagedAffectedPropIdAreaIds.clear(); 381 } 382 383 /** 384 * Gets all the subscription clients for the given propertyID, area ID pair. 385 * 386 * This uses the current state. 387 */ getClients(int propertyId, int areaId)388 public @Nullable Set<ClientType> getClients(int propertyId, int areaId) { 389 if (!mCurrentRateInfoByClientByPropIdAreaId.contains(propertyId, areaId)) { 390 return null; 391 } 392 return mCurrentRateInfoByClientByPropIdAreaId.get(propertyId, areaId).getClients(); 393 } 394 395 /** 396 * Dumps the state. 397 */ 398 @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO) dump(IndentingPrintWriter writer)399 public void dump(IndentingPrintWriter writer) { 400 writer.println("Current subscription states:"); 401 dumpStates(writer, mCurrentRateInfoByClientByPropIdAreaId); 402 writer.println("Staged subscription states:"); 403 dumpStates(writer, mStagedRateInfoByClientByPropIdAreaId); 404 } 405 406 /** 407 * Calculates the difference between the staged state and current state. 408 * 409 * @param outDiffSubscriptions The output subscriptions that has changed. This includes 410 * both new subscriptions and updated subscriptions with a new update rate. 411 * @param outPropertyIdsToUnsubscribe The output property IDs that need to be unsubscribed. 412 */ diffBetweenCurrentAndStage(List<CarSubscription> outDiffSubscriptions, List<Integer> outPropertyIdsToUnsubscribe)413 public void diffBetweenCurrentAndStage(List<CarSubscription> outDiffSubscriptions, 414 List<Integer> outPropertyIdsToUnsubscribe) { 415 if (mStagedAffectedPropIdAreaIds.isEmpty()) { 416 if (VERBOSE) { 417 Slog.v(TAG, "No changes has been staged, no diff"); 418 } 419 return; 420 } 421 ArraySet<Integer> possiblePropIdsToUnsubscribe = new ArraySet<>(); 422 PairSparseArray<RateInfo> diffRateInfoByPropIdAreaId = new PairSparseArray<>(); 423 for (int i = 0; i < mStagedAffectedPropIdAreaIds.size(); i++) { 424 int[] propIdAreaId = mStagedAffectedPropIdAreaIds.valueAt(i); 425 int propertyId = propIdAreaId[0]; 426 int areaId = propIdAreaId[1]; 427 428 if (!mStagedRateInfoByClientByPropIdAreaId.contains(propertyId, areaId)) { 429 // The [PropertyId, areaId] is no longer subscribed. 430 if (DBG) { 431 Slog.d(TAG, String.format("The property: %s, areaId: %s is no longer " 432 + "subscribed", VehiclePropertyIds.toString(propertyId), 433 toAreaIdString(propertyId, areaId))); 434 } 435 possiblePropIdsToUnsubscribe.add(propertyId); 436 continue; 437 } 438 439 RateInfo newCombinedRateInfo = mStagedRateInfoByClientByPropIdAreaId 440 .get(propertyId, areaId).getCombinedRateInfo(); 441 442 if (!mCurrentRateInfoByClientByPropIdAreaId.contains(propertyId, areaId) 443 || !(mCurrentRateInfoByClientByPropIdAreaId 444 .get(propertyId, areaId).getCombinedRateInfo() 445 .equals(newCombinedRateInfo))) { 446 if (DBG) { 447 Slog.d(TAG, String.format( 448 "New combined subscription rate info for property: %s, areaId: %s, %s", 449 VehiclePropertyIds.toString(propertyId), 450 toAreaIdString(propertyId, areaId), newCombinedRateInfo)); 451 } 452 diffRateInfoByPropIdAreaId.put(propertyId, areaId, newCombinedRateInfo); 453 continue; 454 } 455 } 456 outDiffSubscriptions.addAll(getCarSubscription(diffRateInfoByPropIdAreaId)); 457 for (int i = 0; i < possiblePropIdsToUnsubscribe.size(); i++) { 458 int possiblePropIdToUnsubscribe = possiblePropIdsToUnsubscribe.valueAt(i); 459 if (mStagedRateInfoByClientByPropIdAreaId.getSecondKeysForFirstKey( 460 possiblePropIdToUnsubscribe).isEmpty()) { 461 // We should only unsubscribe the property if all area IDs are unsubscribed. 462 if (DBG) { 463 Slog.d(TAG, String.format( 464 "All areas for the property: %s are no longer subscribed, " 465 + "unsubscribe it", VehiclePropertyIds.toString( 466 possiblePropIdToUnsubscribe))); 467 } 468 outPropertyIdsToUnsubscribe.add(possiblePropIdToUnsubscribe); 469 } 470 } 471 } 472 473 /** 474 * Generates the {@code CarSubscription} instances. 475 * 476 * Converts [[propId, areaId] -> updateRateHz] map to 477 * [propId -> [updateRateHz -> list of areaIds]] and then generates subscribe option for each 478 * updateRateHz for each propId. 479 * 480 * @param diffRateInfoByPropIdAreaId A [[propId, areaId] -> updateRateHz] map. 481 */ getCarSubscription( PairSparseArray<RateInfo> diffRateInfoByPropIdAreaId)482 private static List<CarSubscription> getCarSubscription( 483 PairSparseArray<RateInfo> diffRateInfoByPropIdAreaId) { 484 List<CarSubscription> carSubscriptions = new ArrayList<>(); 485 ArraySet<Integer> propertyIds = diffRateInfoByPropIdAreaId.getFirstKeys(); 486 for (int propertyIdIndex = 0; propertyIdIndex < propertyIds.size(); propertyIdIndex++) { 487 int propertyId = propertyIds.valueAt(propertyIdIndex); 488 ArraySet<Integer> areaIds = diffRateInfoByPropIdAreaId.getSecondKeysForFirstKey( 489 propertyId); 490 491 // Group the areaIds by RateInfo. 492 ArrayMap<RateInfo, List<Integer>> areaIdsByRateInfo = new ArrayMap<>(); 493 for (int i = 0; i < areaIds.size(); i++) { 494 int areaId = areaIds.valueAt(i); 495 RateInfo rateInfo = diffRateInfoByPropIdAreaId.get(propertyId, areaId); 496 if (!areaIdsByRateInfo.containsKey(rateInfo)) { 497 areaIdsByRateInfo.put(rateInfo, new ArrayList<>()); 498 } 499 areaIdsByRateInfo.get(rateInfo).add(areaId); 500 } 501 502 // Convert each update rate to a new CarSubscription. 503 for (int i = 0; i < areaIdsByRateInfo.size(); i++) { 504 CarSubscription option = new CarSubscription(); 505 option.propertyId = propertyId; 506 option.areaIds = convertToIntArray(areaIdsByRateInfo.valueAt(i)); 507 option.updateRateHz = areaIdsByRateInfo.keyAt(i).updateRateHz; 508 option.enableVariableUpdateRate = 509 areaIdsByRateInfo.keyAt(i).enableVariableUpdateRate; 510 option.resolution = areaIdsByRateInfo.keyAt(i).resolution; 511 carSubscriptions.add(option); 512 } 513 } 514 515 return carSubscriptions; 516 } 517 cloneCurrentToStageIfClean()518 private void cloneCurrentToStageIfClean() { 519 if (!mStagedAffectedPropIdAreaIds.isEmpty()) { 520 // The current state is not clean, we already cloned once. We allow staging multiple 521 // commits before final commit/drop. 522 return; 523 } 524 525 mStagedRateInfoByClientByPropIdAreaId = new PairSparseArray<>(); 526 for (int i = 0; i < mCurrentRateInfoByClientByPropIdAreaId.size(); i++) { 527 int[] keyPair = mCurrentRateInfoByClientByPropIdAreaId.keyPairAt(i); 528 mStagedRateInfoByClientByPropIdAreaId.put(keyPair[0], keyPair[1], 529 new RateInfoForClients<>( 530 mCurrentRateInfoByClientByPropIdAreaId.valueAt(i))); 531 } 532 } 533 dumpStates(IndentingPrintWriter writer, PairSparseArray<RateInfoForClients<ClientType>> states)534 private static <ClientType> void dumpStates(IndentingPrintWriter writer, 535 PairSparseArray<RateInfoForClients<ClientType>> states) { 536 for (int i = 0; i < states.size(); i++) { 537 int[] propIdAreaId = states.keyPairAt(i); 538 RateInfoForClients<ClientType> rateInfoForClients = states.valueAt(i); 539 int propertyId = propIdAreaId[0]; 540 int areaId = propIdAreaId[1]; 541 Set<ClientType> clients = states.get(propertyId, areaId).getClients(); 542 writer.println("property: " + VehiclePropertyIds.toString(propertyId) 543 + ", area ID: " + toAreaIdString(propertyId, areaId) + " is registered by " 544 + clients.size() 545 + " client(s)."); 546 writer.increaseIndent(); 547 for (ClientType client : clients) { 548 writer.println("Client " + client + ": Subscribed at " 549 + rateInfoForClients.getUpdateRateHz(client) + " hz" 550 + ", enableVur: " 551 + rateInfoForClients.isVariableUpdateRateEnabled(client) 552 + ", resolution: " + rateInfoForClients.getResolution(client)); 553 } 554 writer.decreaseIndent(); 555 } 556 } 557 } 558