blob: f15814599741d1f5b4b441bc44e5ca0b7369feb0 [file] [log] [blame]
// Copyright 2021 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.runtime;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.eventbus.EventBus;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.metrics.GarbageCollectionMetricsUtils;
import com.sun.management.GarbageCollectionNotificationInfo;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import javax.management.Notification;
import javax.management.NotificationEmitter;
import javax.management.NotificationListener;
import javax.management.openmbean.CompositeData;
@ThreadSafe
class MemoryPressureListener implements NotificationListener {
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
private final AtomicReference<EventBus> eventBus = new AtomicReference<>();
private final RetainedHeapLimiter retainedHeapLimiter;
private MemoryPressureListener(RetainedHeapLimiter retainedHeapLimiter) {
this.retainedHeapLimiter = retainedHeapLimiter;
}
@Nullable
static MemoryPressureListener create(RetainedHeapLimiter retainedHeapLimiter) {
return createFromBeans(
ImmutableList.copyOf(ManagementFactory.getGarbageCollectorMXBeans()), retainedHeapLimiter);
}
@VisibleForTesting
@Nullable
static MemoryPressureListener createFromBeans(
ImmutableList<GarbageCollectorMXBean> gcBeans, RetainedHeapLimiter retainedHeapLimiter) {
ImmutableList<NotificationEmitter> tenuredGcEmitters = findTenuredCollectorBeans(gcBeans);
if (tenuredGcEmitters.isEmpty()) {
logger.atSevere().log(
"Unable to find tenured collector from %s: names were %s.",
gcBeans,
gcBeans.stream()
.map(GarbageCollectorMXBean::getMemoryPoolNames)
.map(Arrays::asList)
.collect(toImmutableList()));
return null;
}
MemoryPressureListener memoryPressureListener = new MemoryPressureListener(retainedHeapLimiter);
tenuredGcEmitters.forEach(e -> e.addNotificationListener(memoryPressureListener, null, null));
return memoryPressureListener;
}
@VisibleForTesting
static ImmutableList<NotificationEmitter> findTenuredCollectorBeans(
Iterable<GarbageCollectorMXBean> gcBeans) {
ImmutableList.Builder<NotificationEmitter> builder = ImmutableList.builder();
// Examine all collectors and register for notifications from those which collect the tenured
// space. Normally there is one such collector.
for (GarbageCollectorMXBean gcBean : gcBeans) {
for (String name : gcBean.getMemoryPoolNames()) {
if (GarbageCollectionMetricsUtils.isTenuredSpace(name)) {
builder.add((NotificationEmitter) gcBean);
}
}
}
return builder.build();
}
@Override
public void handleNotification(Notification notification, Object handback) {
if (!notification
.getType()
.equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
return;
}
GarbageCollectionNotificationInfo gcInfo =
GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
long tenuredSpaceUsedBytes = 0L;
long tenuredSpaceMaxBytes = 0L;
for (Map.Entry<String, MemoryUsage> memoryUsageEntry :
gcInfo.getGcInfo().getMemoryUsageAfterGc().entrySet()) {
if (!GarbageCollectionMetricsUtils.isTenuredSpace(memoryUsageEntry.getKey())) {
continue;
}
MemoryUsage space = memoryUsageEntry.getValue();
if (space.getMax() == 0L) {
// The collector sometimes passes us nonsense stats.
continue;
}
tenuredSpaceUsedBytes = space.getUsed();
tenuredSpaceMaxBytes = space.getMax();
break;
}
if (tenuredSpaceMaxBytes == 0L) {
return;
}
MemoryPressureEvent event =
MemoryPressureEvent.newBuilder()
.setWasManualGc(gcInfo.getGcCause().equals("System.gc()"))
.setTenuredSpaceUsedBytes(tenuredSpaceUsedBytes)
.setTenuredSpaceMaxBytes(tenuredSpaceMaxBytes)
.build();
// A null EventBus implies memory pressure event between commands with no active EventBus.
// In such cases, notify RetainedHeapLimiter but do not publish event.
EventBus eventBus = this.eventBus.get();
if (eventBus != null) {
eventBus.post(event);
}
// Post to EventBus first so memory pressure subscribers have a chance to make things
// eligible for GC before RetainedHeapLimiter would trigger a full GC.
this.retainedHeapLimiter.handle(event);
}
void setEventBus(@Nullable EventBus eventBus) {
this.eventBus.set(eventBus);
}
}