// Copyright 2023 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.profiler;

import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.EnumConverter;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import java.time.Duration;
import java.util.Locale;
import javax.annotation.Nullable;
import one.profiler.AsyncProfiler;

/** Bazel module to record a Java Flight Recorder profile for a single command. */
public class CommandProfilerModule extends BlazeModule {

  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

  private static final Duration PROFILING_INTERVAL = Duration.ofMillis(10);

  /** The type of profile to capture. */
  enum ProfileType {
    CPU,
    WALL,
    ALLOC,
    LOCK;

    @Override
    public String toString() {
      return name().toLowerCase(Locale.US);
    }
  }

  /** Options converter for --experimental_command_profile. */
  public static final class ProfileTypeConverter extends EnumConverter<ProfileType> {

    public ProfileTypeConverter() {
      super(ProfileType.class, "--experimental_command_profile setting");
    }
  }

  /** CommandProfilerModule options. */
  public static final class Options extends OptionsBase {

    @Option(
        name = "experimental_command_profile",
        defaultValue = "null",
        converter = ProfileTypeConverter.class,
        documentationCategory = OptionDocumentationCategory.LOGGING,
        effectTags = {OptionEffectTag.UNKNOWN},
        help =
            "Records a Java Flight Recorder profile for the duration of the command. One of the"
                + " supported profiling event types (cpu, wall, alloc or lock) must be given as an"
                + " argument. The profile is written to a file named after the event type under the"
                + " output base directory."
                + " The syntax and semantics of this flag might change in the future to support"
                + " additional profile types or output formats; use at your own risk.")
    public ProfileType profileType;
  }

  @Override
  public Iterable<Class<? extends OptionsBase>> getCommandOptions(Command command) {
    return ImmutableList.of(Options.class);
  }

  @Nullable private ProfileType profileType;
  @Nullable private Reporter reporter;
  @Nullable private Path outputBase;
  @Nullable private Path outputPath;

  @Override
  public void beforeCommand(CommandEnvironment env) {
    Options options = env.getOptions().getOptions(Options.class);
    profileType = options.profileType;
    outputBase = env.getBlazeWorkspace().getOutputBase();
    reporter = env.getReporter();

    if (profileType == null) {
      // Early exit so we don't attempt to load the JNI unless necessary.
      return;
    }

    AsyncProfiler profiler = getProfiler();
    if (profiler == null) {
      return;
    }

    outputPath = getProfilerOutputPath(profileType);

    try {
      profiler.execute(getProfilerCommand(profileType, outputPath));
    } catch (Exception e) {
      // This may occur if the user has insufficient privileges to capture performance events.
      reporter.handle(
          Event.error(String.format("Starting JFR %s profile failed: %s", profileType, e)));
      profileType = null;
    }

    if (profileType != null) {
      reporter.handle(
          Event.info(String.format("Writing JFR %s profile to %s", profileType, outputPath)));
    }
  }

  @Override
  public void afterCommand() {
    if (profileType == null) {
      // Early exit so we don't attempt to load the JNI unless necessary.
      return;
    }

    AsyncProfiler profiler = getProfiler();
    if (profiler == null) {
      return;
    }

    profiler.stop();

    profileType = null;
    outputBase = null;
    reporter = null;
    outputPath = null;
  }

  @Nullable
  private static AsyncProfiler getProfiler() {
    try {
      return AsyncProfiler.getInstance();
    } catch (Throwable t) {
      // Loading the JNI must be allowed to fail, as we might be running on an unsupported platform.
      logger.atWarning().withCause(t).log("Failed to load async_profiler JNI");
    }
    return null;
  }

  private Path getProfilerOutputPath(ProfileType profileType) {
    return outputBase.getChild(profileType + ".jfr");
  }

  private static String getProfilerCommand(ProfileType profileType, Path outputPath) {
    // See https://github.com/async-profiler/async-profiler/blob/master/src/arguments.cpp.
    return String.format(
        "start,event=%s,interval=%s,file=%s,jfr",
        profileType, PROFILING_INTERVAL.toNanos(), outputPath);
  }
}
