// Copyright 2016 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 com.google.devtools.build.lib.util.Preconditions;
import com.sun.management.GarbageCollectionNotificationInfo;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryUsage;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import javax.management.Notification;
import javax.management.NotificationEmitter;
import javax.management.NotificationListener;
import javax.management.openmbean.CompositeData;

/**
 * Monitor the size of the retained heap and exit promptly if it grows too large.  Specifically,
 * check the size of the tenured space after each major GC; if it exceeds 90%, call
 * {@code System.gc()} to trigger a stop-the-world collection; if it's still more than 90% full,
 * exit with an {@link OutOfMemoryError}.
 */
class RetainedHeapLimiter implements NotificationListener {
  private static final Logger LOG = Logger.getLogger(RetainedHeapLimiter.class.getName());
  private static final long MIN_TIME_BETWEEN_TRIGGERED_GC_MILLISECONDS = 60000;

  private boolean installed = false;
  private final AtomicBoolean throwingOom = new AtomicBoolean(false);
  private long lastTriggeredGcInMilliseconds = 0;
  private final int occupiedHeapPercentageThreshold;

  RetainedHeapLimiter(int occupiedHeapPercentageThreshold) {
    this.occupiedHeapPercentageThreshold = occupiedHeapPercentageThreshold;
  }

  void install() {
    Preconditions.checkState(!installed, "RetainedHeapLimiter installed twice");
    installed = true;
    List<GarbageCollectorMXBean> gcbeans = ManagementFactory.getGarbageCollectorMXBeans();
    boolean foundTenured = false;
    // Examine all collectors and register for notifications from those which collect the tenured
    // space. Normally there is one such collector.
    for (GarbageCollectorMXBean gcbean : gcbeans) {
      boolean collectsTenured = false;
      for (String name : gcbean.getMemoryPoolNames()) {
        collectsTenured |= isTenuredSpace(name);
      }
      if (collectsTenured) {
        foundTenured = true;
        NotificationEmitter emitter = (NotificationEmitter) gcbean;
        emitter.addNotificationListener(this, null, null);
      }
    }
    if (!foundTenured) {
      throw new IllegalStateException(
          "Can't find tenured space; update this class for a new collector");
    }
  }

  @Override
  public void handleNotification(Notification notification, Object handback) {
    if (!notification
        .getType()
        .equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
      return;
    }
    GarbageCollectionNotificationInfo info =
        GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
    Map<String, MemoryUsage> spaces = info.getGcInfo().getMemoryUsageAfterGc();
    for (Map.Entry<String, MemoryUsage> entry : spaces.entrySet()) {
      if (isTenuredSpace(entry.getKey())) {
        MemoryUsage space = entry.getValue();
        if (space.getMax() == 0) {
          // The CMS collector sometimes passes us nonsense stats.
          continue;
        }

        long percentUsed = 100 * space.getUsed() / space.getMax();
        if (percentUsed > occupiedHeapPercentageThreshold) {
          if (info.getGcCause().equals("System.gc()") && !throwingOom.getAndSet(true)) {
            // Assume we got here from a GC initiated by the other branch.
            String exitMsg =
                String.format(
                    "RetainedHeapLimiter forcing exit due to GC thrashing: tenured space "
                        + "%s out of %s (>%s%%) occupied after back-to-back full GCs",
                    space.getUsed(),
                    space.getMax(),
                    occupiedHeapPercentageThreshold);
            System.err.println(exitMsg);
            LOG.info(exitMsg);
            // Exits the runtime.
            BugReport.handleCrash(new OutOfMemoryError(exitMsg));
          } else if (System.currentTimeMillis() - lastTriggeredGcInMilliseconds
              > MIN_TIME_BETWEEN_TRIGGERED_GC_MILLISECONDS) {
            LOG.info(
                "Triggering a full GC with "
                    + space.getUsed()
                    + " out of "
                    + space.getMax()
                    + " used");
            // Force a full stop-the-world GC and see if it can get us below the threshold.
            System.gc();
            lastTriggeredGcInMilliseconds = System.currentTimeMillis();
          }
        }
      }
    }
  }

  private static boolean isTenuredSpace(String name) {
    return "CMS Old Gen".equals(name)
        || "G1 Old Gen".equals(name)
        || "PS Old Gen".equals(name)
        || "Tenured Gen".equals(name);
  }
}
