1 /* 2 * Copyright (C) 2020 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.server.power.stats; 18 19 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.hardware.power.stats.EnergyConsumer; 23 import android.hardware.power.stats.EnergyConsumerAttribution; 24 import android.hardware.power.stats.EnergyConsumerResult; 25 import android.hardware.power.stats.EnergyConsumerType; 26 import android.os.BatteryStats.EnergyConsumerDetails; 27 import android.util.Slog; 28 import android.util.SparseArray; 29 import android.util.SparseIntArray; 30 import android.util.SparseLongArray; 31 32 import java.io.PrintWriter; 33 34 /** 35 * Keeps snapshots of data from previously pulled EnergyConsumerResults. 36 */ 37 public class EnergyConsumerSnapshot { 38 private static final String TAG = "EnergyConsumerSnapshot"; 39 40 private static final int MILLIVOLTS_PER_VOLT = 1000; 41 42 public static final long UNAVAILABLE = android.os.BatteryStats.POWER_DATA_UNAVAILABLE; 43 44 /** Map of {@link EnergyConsumer#id} to its corresponding {@link EnergyConsumer}. */ 45 private final SparseArray<EnergyConsumer> mEnergyConsumers; 46 47 /** Number of ordinals for {@link EnergyConsumerType#CPU_CLUSTER}. */ 48 private final int mNumCpuClusterOrdinals; 49 50 /** Number of ordinals for {@link EnergyConsumerType#DISPLAY}. */ 51 private final int mNumDisplayOrdinals; 52 53 /** Number of ordinals for {@link EnergyConsumerType#OTHER}. */ 54 private final int mNumOtherOrdinals; 55 56 /** 57 * Energy snapshots, mapping {@link EnergyConsumer#id} to energy (UJ) from the last time 58 * each {@link EnergyConsumer} was updated. 59 * 60 * Note that the snapshots for different ids may have been taken at different times. 61 * Note that energies for all existing ids are stored here, including each ordinal of type 62 * {@link EnergyConsumerType#OTHER} (tracking their total energy usage). 63 * 64 * If an id is not present yet, it is treated as uninitialized (energy {@link #UNAVAILABLE}). 65 */ 66 private final SparseLongArray mEnergyConsumerSnapshots; 67 68 /** 69 * Voltage snapshots, mapping {@link EnergyConsumer#id} to voltage (mV) from the last time 70 * each {@link EnergyConsumer} was updated. 71 * 72 * see {@link #mEnergyConsumerSnapshots}. 73 */ 74 private final SparseIntArray mVoltageSnapshots; 75 76 /** 77 * Energy snapshots <b>per uid</b> from the last time each {@link EnergyConsumer} of type 78 * {@link EnergyConsumerType#OTHER} was updated. 79 * It maps each OTHER {@link EnergyConsumer#id} to a SparseLongArray, which itself maps each 80 * uid to an energy (UJ). That is, 81 * mAttributionSnapshots.get(consumerId).get(uid) = energy used by uid for this consumer. 82 * 83 * If an id is not present yet, it is treated as uninitialized (i.e. each uid is unavailable). 84 * If an id is present but a uid is not present, that uid's energy is 0. 85 */ 86 private final SparseArray<SparseLongArray> mAttributionSnapshots; 87 88 private EnergyConsumerDetails mEnergyConsumerDetails; 89 90 /** 91 * Constructor that initializes to the given id->EnergyConsumer map, indicating which consumers 92 * exist and what their details are. 93 */ EnergyConsumerSnapshot(@onNull SparseArray<EnergyConsumer> idToConsumerMap)94 EnergyConsumerSnapshot(@NonNull SparseArray<EnergyConsumer> idToConsumerMap) { 95 mEnergyConsumers = idToConsumerMap; 96 mEnergyConsumerSnapshots = new SparseLongArray(mEnergyConsumers.size()); 97 mVoltageSnapshots = new SparseIntArray(mEnergyConsumers.size()); 98 99 mNumCpuClusterOrdinals = calculateNumOrdinals(EnergyConsumerType.CPU_CLUSTER, 100 idToConsumerMap); 101 mNumDisplayOrdinals = calculateNumOrdinals(EnergyConsumerType.DISPLAY, idToConsumerMap); 102 mNumOtherOrdinals = calculateNumOrdinals(EnergyConsumerType.OTHER, idToConsumerMap); 103 mAttributionSnapshots = new SparseArray<>(mNumOtherOrdinals); 104 } 105 106 /** Class for returning the relevant data calculated from the energy consumer delta */ 107 static class EnergyConsumerDeltaData { 108 /** The chargeUC for {@link EnergyConsumerType#BLUETOOTH}. */ 109 public long bluetoothChargeUC = UNAVAILABLE; 110 111 /** The chargeUC for {@link EnergyConsumerType#CPU_CLUSTER}s. */ 112 public long[] cpuClusterChargeUC = null; 113 114 /** The chargeUC for {@link EnergyConsumerType#DISPLAY}. */ 115 public long[] displayChargeUC = null; 116 117 /** The chargeUC for {@link EnergyConsumerType#GNSS}. */ 118 public long gnssChargeUC = UNAVAILABLE; 119 120 /** The chargeUC for {@link EnergyConsumerType#MOBILE_RADIO}. */ 121 public long mobileRadioChargeUC = UNAVAILABLE; 122 123 /** The chargeUC for {@link EnergyConsumerType#WIFI}. */ 124 public long wifiChargeUC = UNAVAILABLE; 125 126 /** The chargeUC for {@link EnergyConsumerType#CAMERA}. */ 127 public long cameraChargeUC = UNAVAILABLE; 128 129 /** Map of {@link EnergyConsumerType#OTHER} ordinals to their total chargeUC. */ 130 public @Nullable long[] otherTotalChargeUC = null; 131 132 /** Map of {@link EnergyConsumerType#OTHER} ordinals to their {uid->chargeUC} maps. */ 133 public @Nullable SparseLongArray[] otherUidChargesUC = null; 134 isEmpty()135 boolean isEmpty() { 136 return bluetoothChargeUC <= 0 137 && isEmpty(cpuClusterChargeUC) 138 && isEmpty(displayChargeUC) 139 && gnssChargeUC <= 0 140 && mobileRadioChargeUC <= 0 141 && wifiChargeUC <= 0 142 && isEmpty(otherTotalChargeUC); 143 } 144 isEmpty(long[] values)145 private boolean isEmpty(long[] values) { 146 if (values == null) { 147 return true; 148 } 149 for (long value: values) { 150 if (value > 0) { 151 return false; 152 } 153 } 154 return true; 155 } 156 } 157 158 /** 159 * Update with the freshly retrieved energy consumers and return the difference (delta) 160 * between the previously stored values and the passed-in values. 161 * 162 * @param ecrs EnergyConsumerResults for some (possibly not all) {@link EnergyConsumer}s. 163 * Consumers that are not present are ignored (they are *not* treated as 0). 164 * @param voltageMV current voltage. 165 * 166 * @return an EnergyConsumerDeltaData, containing maps from the updated consumers to 167 * their corresponding charge deltas. 168 * Fields with no interesting data (consumers not present in ecrs or with no energy 169 * difference) will generally be left as their default values. 170 * otherTotalChargeUC and otherUidChargesUC are always either both null or both of 171 * length {@link #getOtherOrdinalNames().length}. 172 * Returns null, if ecrs is null or empty. 173 */ 174 @Nullable updateAndGetDelta(EnergyConsumerResult[] ecrs, int voltageMV)175 public EnergyConsumerDeltaData updateAndGetDelta(EnergyConsumerResult[] ecrs, int voltageMV) { 176 if (ecrs == null || ecrs.length == 0) { 177 return null; 178 } 179 if (voltageMV <= 0) { 180 Slog.wtf(TAG, "Unexpected battery voltage (" + voltageMV 181 + " mV) when taking energy consumer snapshot"); 182 // TODO (b/181685156): consider adding the nominal voltage to power profile and 183 // falling back to it if measured voltage is unavailable. 184 return null; 185 } 186 final EnergyConsumerDeltaData output = new EnergyConsumerDeltaData(); 187 188 for (final EnergyConsumerResult ecr : ecrs) { 189 // Extract the new energy data for the current consumer. 190 final int consumerId = ecr.id; 191 final long newEnergyUJ = ecr.energyUWs; 192 final EnergyConsumerAttribution[] newAttributions = ecr.attribution; 193 194 // Look up the static information about this consumer. 195 final EnergyConsumer consumer = mEnergyConsumers.get(consumerId, null); 196 if (consumer == null) { 197 Slog.e(TAG, "updateAndGetDelta given invalid consumerId " + consumerId); 198 continue; 199 } 200 final int type = consumer.type; 201 final int ordinal = consumer.ordinal; 202 203 // Look up, and update, the old energy and voltage information about this consumer. 204 final long oldEnergyUJ = mEnergyConsumerSnapshots.get(consumerId, UNAVAILABLE); 205 final int oldVoltageMV = mVoltageSnapshots.get(consumerId); 206 mEnergyConsumerSnapshots.put(consumerId, newEnergyUJ); 207 mVoltageSnapshots.put(consumerId, voltageMV); 208 209 final int avgVoltageMV = (oldVoltageMV + voltageMV + 1) / 2; 210 final SparseLongArray otherUidCharges = 211 updateAndGetDeltaForTypeOther(consumer, newAttributions, avgVoltageMV); 212 // Everything is fully done being updated. We now calculate the delta for returning. 213 214 // NB: Since sum(attribution.energyUWs)<=energyUWs we assume that if deltaEnergy==0 215 // there's no attribution either. Technically that isn't enforced at the HAL, but we 216 // can't really trust data like that anyway. 217 218 if (oldEnergyUJ < 0) continue; // Generally happens only on initialization. 219 if (newEnergyUJ == oldEnergyUJ) continue; 220 221 final long deltaUJ = newEnergyUJ - oldEnergyUJ; 222 if (deltaUJ < 0 || oldVoltageMV <= 0) { 223 Slog.e(TAG, "Bad data! EnergyConsumer " + consumer.name 224 + ": new energy (" + newEnergyUJ + ") < old energy (" + oldEnergyUJ 225 + "), new voltage (" + voltageMV + "), old voltage (" + oldVoltageMV 226 + "). Skipping. "); 227 continue; 228 } 229 230 final long deltaChargeUC = calculateChargeConsumedUC(deltaUJ, avgVoltageMV); 231 switch (type) { 232 case EnergyConsumerType.BLUETOOTH: 233 output.bluetoothChargeUC = deltaChargeUC; 234 break; 235 236 case EnergyConsumerType.CPU_CLUSTER: 237 if (output.cpuClusterChargeUC == null) { 238 output.cpuClusterChargeUC = new long[mNumCpuClusterOrdinals]; 239 } 240 output.cpuClusterChargeUC[ordinal] = deltaChargeUC; 241 break; 242 243 case EnergyConsumerType.DISPLAY: 244 if (output.displayChargeUC == null) { 245 output.displayChargeUC = new long[mNumDisplayOrdinals]; 246 } 247 output.displayChargeUC[ordinal] = deltaChargeUC; 248 break; 249 250 case EnergyConsumerType.GNSS: 251 output.gnssChargeUC = deltaChargeUC; 252 break; 253 254 case EnergyConsumerType.MOBILE_RADIO: 255 output.mobileRadioChargeUC = deltaChargeUC; 256 break; 257 258 case EnergyConsumerType.WIFI: 259 output.wifiChargeUC = deltaChargeUC; 260 break; 261 262 case EnergyConsumerType.CAMERA: 263 output.cameraChargeUC = deltaChargeUC; 264 break; 265 266 case EnergyConsumerType.OTHER: 267 if (output.otherTotalChargeUC == null) { 268 output.otherTotalChargeUC = new long[mNumOtherOrdinals]; 269 output.otherUidChargesUC = new SparseLongArray[mNumOtherOrdinals]; 270 } 271 output.otherTotalChargeUC[ordinal] = deltaChargeUC; 272 output.otherUidChargesUC[ordinal] = otherUidCharges; 273 break; 274 275 default: 276 Slog.w(TAG, "Ignoring consumer " + consumer.name + " of unknown type " + type); 277 278 } 279 } 280 return output; 281 } 282 283 /** 284 * For a consumer of type {@link EnergyConsumerType#OTHER}, updates 285 * {@link #mAttributionSnapshots} with freshly retrieved energy consumers (per uid) and returns 286 * the charge consumed (in microcoulombs) between the previously stored values and the passed-in 287 * values. 288 * 289 * @param consumerInfo a consumer of type {@link EnergyConsumerType#OTHER}. 290 * @param newAttributions Record of uids and their new energyUJ values. 291 * Any uid not present is treated as having energy 0. 292 * If null or empty, all uids are treated as having energy 0. 293 * @param avgVoltageMV The average voltage since the last snapshot. 294 * @return A map (in the sense of {@link EnergyConsumerDeltaData#otherUidChargesUC} for this 295 * consumer) of uid -> chargeDelta, with all uids that have a non-zero chargeDelta. 296 * Returns null if no delta available to calculate. 297 */ updateAndGetDeltaForTypeOther( @onNull EnergyConsumer consumerInfo, @Nullable EnergyConsumerAttribution[] newAttributions, int avgVoltageMV)298 private @Nullable SparseLongArray updateAndGetDeltaForTypeOther( 299 @NonNull EnergyConsumer consumerInfo, 300 @Nullable EnergyConsumerAttribution[] newAttributions, int avgVoltageMV) { 301 302 if (consumerInfo.type != EnergyConsumerType.OTHER) { 303 return null; 304 } 305 if (newAttributions == null) { 306 // Treat null as empty (i.e. all uids have 0 energy). 307 newAttributions = new EnergyConsumerAttribution[0]; 308 } 309 310 // SparseLongArray mapping uid -> energyUJ (for this particular consumerId) 311 SparseLongArray uidOldEnergyMap = mAttributionSnapshots.get(consumerInfo.id, null); 312 313 // If uidOldEnergyMap wasn't present, each uid was UNAVAILABLE, so update data and return. 314 if (uidOldEnergyMap == null) { 315 uidOldEnergyMap = new SparseLongArray(newAttributions.length); 316 mAttributionSnapshots.put(consumerInfo.id, uidOldEnergyMap); 317 for (EnergyConsumerAttribution newAttribution : newAttributions) { 318 uidOldEnergyMap.put(newAttribution.uid, newAttribution.energyUWs); 319 } 320 return null; 321 } 322 323 // Map uid -> chargeDelta. No initial capacity since many deltas might be 0. 324 final SparseLongArray uidChargeDeltas = new SparseLongArray(); 325 326 for (EnergyConsumerAttribution newAttribution : newAttributions) { 327 final int uid = newAttribution.uid; 328 final long newEnergyUJ = newAttribution.energyUWs; 329 // uidOldEnergyMap was present. So any particular uid that wasn't present, had 0 energy. 330 final long oldEnergyUJ = uidOldEnergyMap.get(uid, 0L); 331 uidOldEnergyMap.put(uid, newEnergyUJ); 332 333 // Everything is fully done being updated. We now calculate the delta for returning. 334 if (oldEnergyUJ < 0) continue; 335 if (newEnergyUJ == oldEnergyUJ) continue; 336 final long deltaUJ = newEnergyUJ - oldEnergyUJ; 337 if (deltaUJ < 0 || avgVoltageMV <= 0) { 338 Slog.e(TAG, "EnergyConsumer " + consumerInfo.name + ": new energy (" + newEnergyUJ 339 + ") but old energy (" + oldEnergyUJ + "). Average voltage (" + avgVoltageMV 340 + ")Skipping. "); 341 continue; 342 } 343 344 final long deltaChargeUC = calculateChargeConsumedUC(deltaUJ, avgVoltageMV); 345 uidChargeDeltas.put(uid, deltaChargeUC); 346 } 347 return uidChargeDeltas; 348 } 349 350 /** Dump debug data. */ dump(PrintWriter pw)351 public void dump(PrintWriter pw) { 352 pw.println("Energy consumer snapshot"); 353 pw.println("List of EnergyConsumers:"); 354 for (int i = 0; i < mEnergyConsumers.size(); i++) { 355 final int id = mEnergyConsumers.keyAt(i); 356 final EnergyConsumer consumer = mEnergyConsumers.valueAt(i); 357 pw.println(String.format(" Consumer %d is {id=%d, ordinal=%d, type=%d, name=%s}", id, 358 consumer.id, consumer.ordinal, consumer.type, consumer.name)); 359 } 360 pw.println("Map of consumerIds to energy (in microjoules):"); 361 for (int i = 0; i < mEnergyConsumerSnapshots.size(); i++) { 362 final int id = mEnergyConsumerSnapshots.keyAt(i); 363 final long energyUJ = mEnergyConsumerSnapshots.valueAt(i); 364 final long voltageMV = mVoltageSnapshots.valueAt(i); 365 pw.println(String.format(" Consumer %d has energy %d uJ at %d mV", id, energyUJ, 366 voltageMV)); 367 } 368 pw.println("List of the " + mNumOtherOrdinals + " OTHER EnergyConsumers:"); 369 pw.println(" " + mAttributionSnapshots); 370 pw.println(); 371 } 372 373 /** 374 * Returns the names of ordinals for {@link EnergyConsumerType#OTHER}, i.e. the names of 375 * custom energy buckets supported by the device. 376 */ getOtherOrdinalNames()377 public String[] getOtherOrdinalNames() { 378 final String[] names = new String[mNumOtherOrdinals]; 379 int consumerIndex = 0; 380 final int size = mEnergyConsumers.size(); 381 for (int idx = 0; idx < size; idx++) { 382 final EnergyConsumer consumer = mEnergyConsumers.valueAt(idx); 383 if (consumer.type == (int) EnergyConsumerType.OTHER) { 384 names[consumerIndex++] = sanitizeCustomBucketName(consumer.name); 385 } 386 } 387 return names; 388 } 389 sanitizeCustomBucketName(String bucketName)390 private String sanitizeCustomBucketName(String bucketName) { 391 if (bucketName == null) { 392 return ""; 393 } 394 StringBuilder sb = new StringBuilder(bucketName.length()); 395 for (char c : bucketName.toCharArray()) { 396 if (Character.isWhitespace(c)) { 397 sb.append(' '); 398 } else if (Character.isISOControl(c)) { 399 sb.append('_'); 400 } else { 401 sb.append(c); 402 } 403 } 404 return sb.toString(); 405 } 406 407 /** Determines the number of ordinals for a given {@link EnergyConsumerType}. */ calculateNumOrdinals(@nergyConsumerType int type, SparseArray<EnergyConsumer> idToConsumer)408 private static int calculateNumOrdinals(@EnergyConsumerType int type, 409 SparseArray<EnergyConsumer> idToConsumer) { 410 if (idToConsumer == null) return 0; 411 int numOrdinals = 0; 412 final int size = idToConsumer.size(); 413 for (int idx = 0; idx < size; idx++) { 414 final EnergyConsumer consumer = idToConsumer.valueAt(idx); 415 if (consumer.type == type) numOrdinals++; 416 } 417 return numOrdinals; 418 } 419 420 /** Calculate charge consumption (in microcoulombs) from a given energy and voltage */ calculateChargeConsumedUC(long deltaEnergyUJ, int avgVoltageMV)421 private long calculateChargeConsumedUC(long deltaEnergyUJ, int avgVoltageMV) { 422 // To overflow, a 3.7V 10000mAh battery would need to completely drain 69244 times 423 // since the last snapshot. Round off to the nearest whole long. 424 return (deltaEnergyUJ * MILLIVOLTS_PER_VOLT + (avgVoltageMV / 2)) / avgVoltageMV; 425 } 426 427 /** 428 * Converts the EnergyConsumerDeltaData object to EnergyConsumerDetails, which can 429 * be saved in battery history. 430 */ getEnergyConsumerDetails( EnergyConsumerDeltaData delta)431 EnergyConsumerDetails getEnergyConsumerDetails( 432 EnergyConsumerDeltaData delta) { 433 if (mEnergyConsumerDetails == null) { 434 mEnergyConsumerDetails = createEnergyConsumerDetails(); 435 } 436 437 final long[] chargeUC = mEnergyConsumerDetails.chargeUC; 438 for (int i = 0; i < mEnergyConsumerDetails.consumers.length; i++) { 439 EnergyConsumerDetails.EnergyConsumer energyConsumer = 440 mEnergyConsumerDetails.consumers[i]; 441 switch (energyConsumer.type) { 442 case EnergyConsumerType.BLUETOOTH: 443 chargeUC[i] = delta.bluetoothChargeUC; 444 break; 445 case EnergyConsumerType.CPU_CLUSTER: 446 if (delta.cpuClusterChargeUC != null) { 447 chargeUC[i] = delta.cpuClusterChargeUC[energyConsumer.ordinal]; 448 } else { 449 chargeUC[i] = UNAVAILABLE; 450 } 451 break; 452 case EnergyConsumerType.DISPLAY: 453 if (delta.displayChargeUC != null) { 454 chargeUC[i] = delta.displayChargeUC[energyConsumer.ordinal]; 455 } else { 456 chargeUC[i] = UNAVAILABLE; 457 } 458 break; 459 case EnergyConsumerType.GNSS: 460 chargeUC[i] = delta.gnssChargeUC; 461 break; 462 case EnergyConsumerType.MOBILE_RADIO: 463 chargeUC[i] = delta.mobileRadioChargeUC; 464 break; 465 case EnergyConsumerType.WIFI: 466 chargeUC[i] = delta.wifiChargeUC; 467 break; 468 case EnergyConsumerType.CAMERA: 469 chargeUC[i] = delta.cameraChargeUC; 470 break; 471 case EnergyConsumerType.OTHER: 472 if (delta.otherTotalChargeUC != null) { 473 chargeUC[i] = delta.otherTotalChargeUC[energyConsumer.ordinal]; 474 } else { 475 chargeUC[i] = UNAVAILABLE; 476 } 477 break; 478 default: 479 chargeUC[i] = UNAVAILABLE; 480 break; 481 } 482 } 483 return mEnergyConsumerDetails; 484 } 485 createEnergyConsumerDetails()486 private EnergyConsumerDetails createEnergyConsumerDetails() { 487 EnergyConsumerDetails details = new EnergyConsumerDetails(); 488 details.consumers = 489 new EnergyConsumerDetails.EnergyConsumer[mEnergyConsumers.size()]; 490 for (int i = 0; i < mEnergyConsumers.size(); i++) { 491 EnergyConsumer energyConsumer = mEnergyConsumers.valueAt(i); 492 EnergyConsumerDetails.EnergyConsumer consumer = 493 new EnergyConsumerDetails.EnergyConsumer(); 494 consumer.type = energyConsumer.type; 495 consumer.ordinal = energyConsumer.ordinal; 496 switch (consumer.type) { 497 case EnergyConsumerType.BLUETOOTH: 498 consumer.name = "BLUETOOTH"; 499 break; 500 case EnergyConsumerType.CPU_CLUSTER: 501 consumer.name = "CPU"; 502 break; 503 case EnergyConsumerType.DISPLAY: 504 consumer.name = "DISPLAY"; 505 break; 506 case EnergyConsumerType.GNSS: 507 consumer.name = "GNSS"; 508 break; 509 case EnergyConsumerType.MOBILE_RADIO: 510 consumer.name = "MOBILE_RADIO"; 511 break; 512 case EnergyConsumerType.WIFI: 513 consumer.name = "WIFI"; 514 break; 515 case EnergyConsumerType.OTHER: 516 consumer.name = sanitizeCustomBucketName(energyConsumer.name); 517 break; 518 default: 519 consumer.name = "UNKNOWN"; 520 break; 521 } 522 if (consumer.type != EnergyConsumerType.OTHER) { 523 boolean hasOrdinal = consumer.ordinal != 0; 524 if (!hasOrdinal) { 525 // See if any other EnergyConsumer of the same type has an ordinal 526 for (int j = 0; j < mEnergyConsumers.size(); j++) { 527 EnergyConsumer aConsumer = mEnergyConsumers.valueAt(j); 528 if (aConsumer.type == consumer.type && aConsumer.ordinal != 0) { 529 hasOrdinal = true; 530 break; 531 } 532 } 533 } 534 if (hasOrdinal) { 535 consumer.name = consumer.name + "/" + energyConsumer.ordinal; 536 } 537 } 538 details.consumers[i] = consumer; 539 } 540 541 details.chargeUC = new long[details.consumers.length]; 542 return details; 543 } 544 } 545