blob: 3a02010783871c9889fac11b3b8d185849c2db54 [file] [log] [blame]
/*
* Copyright (C) 2017 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 com.android.car;
import android.annotation.Nullable;
import android.car.annotation.FutureFeature;
import android.car.hardware.CarDiagnosticEvent;
import android.car.hardware.CarDiagnosticManager;
import android.car.hardware.ICarDiagnostic;
import android.car.hardware.ICarDiagnosticEventListener;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.Log;
import com.android.car.Listeners.ClientWithRate;
import com.android.car.hal.DiagnosticHalService;
import com.android.internal.annotations.GuardedBy;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
@FutureFeature
/** @hide */
public class CarDiagnosticService extends ICarDiagnostic.Stub
implements CarServiceBase, DiagnosticHalService.DiagnosticListener {
/** {@link #mDiagnosticLock} is not waited forever for handling disconnection */
private static final long MAX_DIAGNOSTIC_LOCK_WAIT_MS = 1000;
/** lock to access diagnostic structures */
private final ReentrantLock mDiagnosticLock = new ReentrantLock();
/** hold clients callback */
@GuardedBy("mDiagnosticLock")
private final LinkedList<DiagnosticClient> mClients = new LinkedList<>();
/** key: diagnostic type. */
@GuardedBy("mDiagnosticLock")
private final HashMap<Integer, Listeners<DiagnosticClient>> mDiagnosticListeners =
new HashMap<>();
/** the latest live frame data. */
@GuardedBy("mDiagnosticLock")
private final LiveFrameRecord mLiveFrameDiagnosticRecord = new LiveFrameRecord(mDiagnosticLock);
/** the latest freeze frame data (key: DTC) */
@GuardedBy("mDiagnosticLock")
private final FreezeFrameRecord mFreezeFrameDiagnosticRecords = new FreezeFrameRecord(
mDiagnosticLock);
private final DiagnosticHalService mDiagnosticHal;
private final Context mContext;
public CarDiagnosticService(Context context, DiagnosticHalService diagnosticHal) {
mContext = context;
mDiagnosticHal = diagnosticHal;
}
@Override
public void init() {
mDiagnosticLock.lock();
try {
mDiagnosticHal.setDiagnosticListener(this);
setInitialLiveFrame();
setInitialFreezeFrames();
} finally {
mDiagnosticLock.unlock();
}
}
private CarDiagnosticEvent setInitialLiveFrame() {
CarDiagnosticEvent liveFrame = setRecentmostLiveFrame(mDiagnosticHal.getCurrentLiveFrame());
return liveFrame;
}
private void setInitialFreezeFrames() {
for (long timestamp: mDiagnosticHal.getFreezeFrameTimestamps()) {
setRecentmostFreezeFrame(mDiagnosticHal.getFreezeFrame(timestamp));
}
}
private CarDiagnosticEvent setRecentmostLiveFrame(final CarDiagnosticEvent event) {
return mLiveFrameDiagnosticRecord.update(Objects.requireNonNull(event).checkLiveFrame());
}
private CarDiagnosticEvent setRecentmostFreezeFrame(final CarDiagnosticEvent event) {
return mFreezeFrameDiagnosticRecords.update(
Objects.requireNonNull(event).checkFreezeFrame());
}
@Override
public void release() {
mDiagnosticLock.lock();
try {
mDiagnosticListeners.forEach(
(Integer frameType, Listeners diagnosticListeners) ->
diagnosticListeners.release());
mDiagnosticListeners.clear();
mLiveFrameDiagnosticRecord.disableIfNeeded();
mFreezeFrameDiagnosticRecords.disableIfNeeded();
mClients.clear();
} finally {
mDiagnosticLock.unlock();
}
}
private void processDiagnosticData(List<CarDiagnosticEvent> events) {
ArrayMap<CarDiagnosticService.DiagnosticClient, List<CarDiagnosticEvent>> eventsByClient =
new ArrayMap<>();
Listeners<DiagnosticClient> listeners = null;
mDiagnosticLock.lock();
for (CarDiagnosticEvent event : events) {
if (event.isLiveFrame()) {
// record recent-most live frame information
setRecentmostLiveFrame(event);
listeners = mDiagnosticListeners.get(CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE);
} else if (event.isFreezeFrame()) {
setRecentmostFreezeFrame(event);
listeners = mDiagnosticListeners.get(CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE);
} else {
Log.w(
CarLog.TAG_DIAGNOSTIC,
String.format("received unknown diagnostic event: %s", event));
continue;
}
if (null != listeners) {
for (ClientWithRate<DiagnosticClient> clientWithRate : listeners.getClients()) {
DiagnosticClient client = clientWithRate.getClient();
List<CarDiagnosticEvent> clientEvents = eventsByClient.computeIfAbsent(client,
(DiagnosticClient diagnosticClient) -> new LinkedList<>());
clientEvents.add(event);
}
}
}
mDiagnosticLock.unlock();
for (ArrayMap.Entry<CarDiagnosticService.DiagnosticClient, List<CarDiagnosticEvent>> entry :
eventsByClient.entrySet()) {
CarDiagnosticService.DiagnosticClient client = entry.getKey();
List<CarDiagnosticEvent> clientEvents = entry.getValue();
client.dispatchDiagnosticUpdate(clientEvents);
}
}
/** Received diagnostic data from car. */
@Override
public void onDiagnosticEvents(List<CarDiagnosticEvent> events) {
processDiagnosticData(events);
}
private List<CarDiagnosticEvent> getCachedEventsLocked(int frameType) {
ArrayList<CarDiagnosticEvent> events = new ArrayList<>();
switch (frameType) {
case CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE:
mLiveFrameDiagnosticRecord.lock();
events.add(mLiveFrameDiagnosticRecord.getLastEvent());
mLiveFrameDiagnosticRecord.unlock();
break;
case CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE:
mFreezeFrameDiagnosticRecords.lock();
mFreezeFrameDiagnosticRecords.getEvents().forEach(events::add);
mFreezeFrameDiagnosticRecords.unlock();
break;
default: break;
}
return events;
}
private void assertPermission(int frameType) {
if (Binder.getCallingUid() != Process.myUid()) {
switch (getDiagnosticPermission(frameType)) {
case PackageManager.PERMISSION_GRANTED:
break;
default:
throw new SecurityException(
"client does not have permission:"
+ getPermissionName(frameType)
+ " pid:"
+ Binder.getCallingPid()
+ " uid:"
+ Binder.getCallingUid());
}
}
}
@Override
public boolean registerOrUpdateDiagnosticListener(int frameType, int rate,
ICarDiagnosticEventListener listener) {
boolean shouldStartDiagnostics = false;
CarDiagnosticService.DiagnosticClient diagnosticClient = null;
Integer oldRate = null;
Listeners<DiagnosticClient> diagnosticListeners = null;
mDiagnosticLock.lock();
try {
assertPermission(frameType);
diagnosticClient = findDiagnosticClientLocked(listener);
Listeners.ClientWithRate<DiagnosticClient> diagnosticClientWithRate = null;
if (diagnosticClient == null) {
diagnosticClient = new DiagnosticClient(listener);
try {
listener.asBinder().linkToDeath(diagnosticClient, 0);
} catch (RemoteException e) {
Log.w(
CarLog.TAG_DIAGNOSTIC,
String.format(
"received RemoteException trying to register listener for %s",
frameType));
return false;
}
mClients.add(diagnosticClient);
}
// If we have a cached event for this diagnostic, send the event.
diagnosticClient.dispatchDiagnosticUpdate(getCachedEventsLocked(frameType));
diagnosticListeners = mDiagnosticListeners.get(frameType);
if (diagnosticListeners == null) {
diagnosticListeners = new Listeners<>(rate);
mDiagnosticListeners.put(frameType, diagnosticListeners);
shouldStartDiagnostics = true;
} else {
oldRate = diagnosticListeners.getRate();
diagnosticClientWithRate =
diagnosticListeners.findClientWithRate(diagnosticClient);
}
if (diagnosticClientWithRate == null) {
diagnosticClientWithRate =
new ClientWithRate<>(diagnosticClient, rate);
diagnosticListeners.addClientWithRate(diagnosticClientWithRate);
} else {
diagnosticClientWithRate.setRate(rate);
}
if (diagnosticListeners.getRate() > rate) {
diagnosticListeners.setRate(rate);
shouldStartDiagnostics = true;
}
diagnosticClient.addDiagnostic(frameType);
} finally {
mDiagnosticLock.unlock();
}
Log.i(
CarLog.TAG_DIAGNOSTIC,
String.format(
"shouldStartDiagnostics = %s for %s at rate %d",
shouldStartDiagnostics, frameType, rate));
// start diagnostic outside lock as it can take time.
if (shouldStartDiagnostics) {
if (!startDiagnostic(frameType, rate)) {
// failed. so remove from active diagnostic list.
Log.w(CarLog.TAG_DIAGNOSTIC, "startDiagnostic failed");
mDiagnosticLock.lock();
try {
diagnosticClient.removeDiagnostic(frameType);
if (oldRate != null) {
diagnosticListeners.setRate(oldRate);
} else {
mDiagnosticListeners.remove(frameType);
}
} finally {
mDiagnosticLock.unlock();
}
return false;
}
}
return true;
}
//TODO(egranata): handle permissions correctly
private int getDiagnosticPermission(int frameType) {
String permission = getPermissionName(frameType);
int result = PackageManager.PERMISSION_GRANTED;
if (permission != null) {
return mContext.checkCallingOrSelfPermission(permission);
}
// If no permission is required, return granted.
return result;
}
private String getPermissionName(int frameType) {
return null;
}
private boolean startDiagnostic(int frameType, int rate) {
Log.i(CarLog.TAG_DIAGNOSTIC, String.format("starting diagnostic %s at rate %d",
frameType, rate));
DiagnosticHalService diagnosticHal = getDiagnosticHal();
if (diagnosticHal != null) {
if (!diagnosticHal.isReady()) {
Log.w(CarLog.TAG_DIAGNOSTIC, "diagnosticHal not ready");
return false;
}
switch (frameType) {
case CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE:
if (mLiveFrameDiagnosticRecord.isEnabled()) {
return true;
}
if (diagnosticHal.requestSensorStart(CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE,
rate)) {
mLiveFrameDiagnosticRecord.enable();
return true;
}
break;
case CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE:
if (mFreezeFrameDiagnosticRecords.isEnabled()) {
return true;
}
if (diagnosticHal.requestSensorStart(CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE,
rate)) {
mFreezeFrameDiagnosticRecords.enable();
return true;
}
break;
}
}
return false;
}
@Override
public void unregisterDiagnosticListener(
int frameType, ICarDiagnosticEventListener listener) {
boolean shouldStopDiagnostic = false;
boolean shouldRestartDiagnostic = false;
int newRate = 0;
mDiagnosticLock.lock();
try {
DiagnosticClient diagnosticClient = findDiagnosticClientLocked(listener);
if (diagnosticClient == null) {
Log.i(
CarLog.TAG_DIAGNOSTIC,
String.format(
"trying to unregister diagnostic client %s for %s which is not registered",
listener, frameType));
// never registered or already unregistered.
return;
}
diagnosticClient.removeDiagnostic(frameType);
if (diagnosticClient.getNumberOfActiveDiagnostic() == 0) {
diagnosticClient.release();
mClients.remove(diagnosticClient);
}
Listeners<DiagnosticClient> diagnosticListeners = mDiagnosticListeners.get(frameType);
if (diagnosticListeners == null) {
// diagnostic not active
return;
}
ClientWithRate<DiagnosticClient> clientWithRate =
diagnosticListeners.findClientWithRate(diagnosticClient);
if (clientWithRate == null) {
return;
}
diagnosticListeners.removeClientWithRate(clientWithRate);
if (diagnosticListeners.getNumberOfClients() == 0) {
shouldStopDiagnostic = true;
mDiagnosticListeners.remove(frameType);
} else if (diagnosticListeners.updateRate()) { // rate changed
newRate = diagnosticListeners.getRate();
shouldRestartDiagnostic = true;
}
} finally {
mDiagnosticLock.unlock();
}
Log.i(
CarLog.TAG_DIAGNOSTIC,
String.format(
"shouldStopDiagnostic = %s, shouldRestartDiagnostic = %s for type %s",
shouldStopDiagnostic, shouldRestartDiagnostic, frameType));
if (shouldStopDiagnostic) {
stopDiagnostic(frameType);
} else if (shouldRestartDiagnostic) {
startDiagnostic(frameType, newRate);
}
}
private void stopDiagnostic(int frameType) {
DiagnosticHalService diagnosticHal = getDiagnosticHal();
if (diagnosticHal == null || !diagnosticHal.isReady()) {
Log.w(CarLog.TAG_DIAGNOSTIC, "diagnosticHal not ready");
return;
}
switch (frameType) {
case CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE:
if (mLiveFrameDiagnosticRecord.disableIfNeeded())
diagnosticHal.requestSensorStop(CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE);
break;
case CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE:
if (mFreezeFrameDiagnosticRecords.disableIfNeeded())
diagnosticHal.requestSensorStop(CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE);
break;
}
}
private DiagnosticHalService getDiagnosticHal() {
return mDiagnosticHal;
}
// ICarDiagnostic implementations
@Override
public CarDiagnosticEvent getLatestLiveFrame() {
mLiveFrameDiagnosticRecord.lock();
CarDiagnosticEvent liveFrame = mLiveFrameDiagnosticRecord.getLastEvent();
mLiveFrameDiagnosticRecord.unlock();
return liveFrame;
}
@Override
public long[] getFreezeFrameTimestamps() {
mFreezeFrameDiagnosticRecords.lock();
long[] timestamps = mFreezeFrameDiagnosticRecords.getFreezeFrameTimestamps();
mFreezeFrameDiagnosticRecords.unlock();
return timestamps;
}
@Override
@Nullable
public CarDiagnosticEvent getFreezeFrame(long timestamp) {
mFreezeFrameDiagnosticRecords.lock();
CarDiagnosticEvent freezeFrame = mFreezeFrameDiagnosticRecords.getEvent(timestamp);
mFreezeFrameDiagnosticRecords.unlock();
return freezeFrame;
}
@Override
public boolean clearFreezeFrames(long... timestamps) {
mFreezeFrameDiagnosticRecords.lock();
mDiagnosticHal.clearFreezeFrames(timestamps);
mFreezeFrameDiagnosticRecords.clearEvents();
mFreezeFrameDiagnosticRecords.unlock();
return true;
}
/**
* Find DiagnosticClient from client list and return it. This should be called with mClients
* locked.
*
* @param listener
* @return null if not found.
*/
private CarDiagnosticService.DiagnosticClient findDiagnosticClientLocked(
ICarDiagnosticEventListener listener) {
IBinder binder = listener.asBinder();
for (DiagnosticClient diagnosticClient : mClients) {
if (diagnosticClient.isHoldingListenerBinder(binder)) {
return diagnosticClient;
}
}
return null;
}
private void removeClient(DiagnosticClient diagnosticClient) {
mDiagnosticLock.lock();
try {
for (int diagnostic : diagnosticClient.getDiagnosticArray()) {
unregisterDiagnosticListener(
diagnostic, diagnosticClient.getICarDiagnosticEventListener());
}
mClients.remove(diagnosticClient);
} finally {
mDiagnosticLock.unlock();
}
}
private class DiagnosticDispatchHandler extends Handler {
private static final long DIAGNOSTIC_DISPATCH_MIN_INTERVAL_MS = 16; // over 60Hz
private static final int MSG_DIAGNOSTIC_DATA = 0;
private long mLastDiagnosticDispatchTime = -1;
private int mFreeListIndex = 0;
private final LinkedList<CarDiagnosticEvent>[] mDiagnosticDataList = new LinkedList[2];
private DiagnosticDispatchHandler(Looper looper) {
super(looper);
for (int i = 0; i < mDiagnosticDataList.length; i++) {
mDiagnosticDataList[i] = new LinkedList<CarDiagnosticEvent>();
}
}
private synchronized void handleDiagnosticEvents(List<CarDiagnosticEvent> data) {
LinkedList<CarDiagnosticEvent> list = mDiagnosticDataList[mFreeListIndex];
list.addAll(data);
requestDispatchLocked();
}
private synchronized void handleDiagnosticEvent(CarDiagnosticEvent event) {
LinkedList<CarDiagnosticEvent> list = mDiagnosticDataList[mFreeListIndex];
list.add(event);
requestDispatchLocked();
}
private void requestDispatchLocked() {
Message msg = obtainMessage(MSG_DIAGNOSTIC_DATA);
long now = SystemClock.uptimeMillis();
long delta = now - mLastDiagnosticDispatchTime;
if (delta > DIAGNOSTIC_DISPATCH_MIN_INTERVAL_MS) {
sendMessage(msg);
} else {
sendMessageDelayed(msg, DIAGNOSTIC_DISPATCH_MIN_INTERVAL_MS - delta);
}
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DIAGNOSTIC_DATA:
doHandleDiagnosticData();
break;
default:
break;
}
}
private void doHandleDiagnosticData() {
List<CarDiagnosticEvent> listToDispatch = null;
synchronized (this) {
mLastDiagnosticDispatchTime = SystemClock.uptimeMillis();
int nonFreeListIndex = mFreeListIndex ^ 0x1;
List<CarDiagnosticEvent> nonFreeList = mDiagnosticDataList[nonFreeListIndex];
List<CarDiagnosticEvent> freeList = mDiagnosticDataList[mFreeListIndex];
if (nonFreeList.size() > 0) {
// copy again, but this should not be normal case
nonFreeList.addAll(freeList);
listToDispatch = nonFreeList;
freeList.clear();
} else if (freeList.size() > 0) {
listToDispatch = freeList;
mFreeListIndex = nonFreeListIndex;
}
}
// leave this part outside lock so that time-taking dispatching can be done without
// blocking diagnostic event notification.
if (listToDispatch != null) {
processDiagnosticData(listToDispatch);
listToDispatch.clear();
}
}
}
/** internal instance for pending client request */
private class DiagnosticClient implements Listeners.IListener {
/** callback for diagnostic events */
private final ICarDiagnosticEventListener mListener;
private final Set<Integer> mActiveDiagnostics = new HashSet<>();
/** when false, it is already released */
private volatile boolean mActive = true;
DiagnosticClient(ICarDiagnosticEventListener listener) {
this.mListener = listener;
}
@Override
public boolean equals(Object o) {
if (o instanceof CarDiagnosticService.DiagnosticClient
&& mListener.asBinder()
== ((CarDiagnosticService.DiagnosticClient) o).mListener.asBinder()) {
return true;
}
return false;
}
boolean isHoldingListenerBinder(IBinder listenerBinder) {
return mListener.asBinder() == listenerBinder;
}
void addDiagnostic(int frameType) {
mActiveDiagnostics.add(frameType);
}
void removeDiagnostic(int frameType) {
mActiveDiagnostics.remove(frameType);
}
int getNumberOfActiveDiagnostic() {
return mActiveDiagnostics.size();
}
int[] getDiagnosticArray() {
return mActiveDiagnostics.stream().mapToInt(Integer::intValue).toArray();
}
ICarDiagnosticEventListener getICarDiagnosticEventListener() {
return mListener;
}
/** Client dead. should remove all diagnostic requests from client */
@Override
public void binderDied() {
mListener.asBinder().unlinkToDeath(this, 0);
removeClient(this);
}
void dispatchDiagnosticUpdate(List<CarDiagnosticEvent> events) {
if (events.size() == 0) {
return;
}
if (mActive) {
try {
mListener.onDiagnosticEvents(events);
} catch (RemoteException e) {
//ignore. crash will be handled by death handler
}
} else {
}
}
@Override
public void release() {
if (mActive) {
mListener.asBinder().unlinkToDeath(this, 0);
mActiveDiagnostics.clear();
mActive = false;
}
}
}
private static abstract class DiagnosticRecord {
private final ReentrantLock mLock;
protected boolean mEnabled = false;
DiagnosticRecord(ReentrantLock lock) {
mLock = lock;
}
void lock() {
mLock.lock();
}
void unlock() {
mLock.unlock();
}
boolean isEnabled() {
return mEnabled;
}
void enable() {
mEnabled = true;
}
abstract boolean disableIfNeeded();
abstract CarDiagnosticEvent update(CarDiagnosticEvent newEvent);
}
private static class LiveFrameRecord extends DiagnosticRecord {
/** Store the most recent live-frame. */
CarDiagnosticEvent mLastEvent = null;
LiveFrameRecord(ReentrantLock lock) {
super(lock);
}
@Override
boolean disableIfNeeded() {
if (!mEnabled) return false;
mEnabled = false;
mLastEvent = null;
return true;
}
@Override
CarDiagnosticEvent update(CarDiagnosticEvent newEvent) {
newEvent = Objects.requireNonNull(newEvent);
if((null == mLastEvent) || mLastEvent.isEarlierThan(newEvent))
mLastEvent = newEvent;
return mLastEvent;
}
CarDiagnosticEvent getLastEvent() {
return mLastEvent;
}
}
private static class FreezeFrameRecord extends DiagnosticRecord {
/** Store the timestamp --> freeze frame mapping. */
HashMap<Long, CarDiagnosticEvent> mEvents = new HashMap<>();
FreezeFrameRecord(ReentrantLock lock) {
super(lock);
}
@Override
boolean disableIfNeeded() {
if (!mEnabled) return false;
mEnabled = false;
clearEvents();
return true;
}
void clearEvents() {
mEvents.clear();
}
@Override
CarDiagnosticEvent update(CarDiagnosticEvent newEvent) {
mEvents.put(newEvent.timestamp, newEvent);
return newEvent;
}
long[] getFreezeFrameTimestamps() {
return mEvents.keySet().stream().mapToLong(Long::longValue).toArray();
}
CarDiagnosticEvent getEvent(long timestamp) {
return mEvents.get(timestamp);
}
Iterable<CarDiagnosticEvent> getEvents() {
return mEvents.values();
}
}
@Override
public void dump(PrintWriter writer) {
writer.println("*CarDiagnosticService*");
writer.println("**last events for diagnostics**");
if (null != mLiveFrameDiagnosticRecord.getLastEvent()) {
writer.println("last live frame event: ");
writer.println(mLiveFrameDiagnosticRecord.getLastEvent());
}
writer.println("freeze frame events: ");
mFreezeFrameDiagnosticRecords.getEvents().forEach(writer::println);
writer.println("**clients**");
try {
for (DiagnosticClient client : mClients) {
if (client != null) {
try {
writer.println(
"binder:"
+ client.mListener
+ " active diagnostics:"
+ Arrays.toString(client.getDiagnosticArray()));
} catch (ConcurrentModificationException e) {
writer.println("concurrent modification happened");
}
} else {
writer.println("null client");
}
}
} catch (ConcurrentModificationException e) {
writer.println("concurrent modification happened");
}
writer.println("**diagnostic listeners**");
try {
for (int diagnostic : mDiagnosticListeners.keySet()) {
Listeners diagnosticListeners = mDiagnosticListeners.get(diagnostic);
if (diagnosticListeners != null) {
writer.println(
" Diagnostic:"
+ diagnostic
+ " num client:"
+ diagnosticListeners.getNumberOfClients()
+ " rate:"
+ diagnosticListeners.getRate());
}
}
} catch (ConcurrentModificationException e) {
writer.println("concurrent modification happened");
}
}
}