// Copyright 2014 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.commands;

import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.profiler.ProfileInfo;
import com.google.devtools.build.lib.profiler.ProfileInfo.InfoListener;
import com.google.devtools.build.lib.profiler.ProfileInfo.Task;
import com.google.devtools.build.lib.profiler.ProfilePhase;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.profiler.output.HtmlCreator;
import com.google.devtools.build.lib.profiler.output.PhaseText;
import com.google.devtools.build.lib.profiler.statistics.CriticalPathStatistics;
import com.google.devtools.build.lib.profiler.statistics.MultiProfileStatistics;
import com.google.devtools.build.lib.profiler.statistics.PhaseStatistics;
import com.google.devtools.build.lib.profiler.statistics.PhaseSummaryStatistics;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.StringUtil;
import com.google.devtools.build.lib.util.TimeUtilities;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsProvider;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.EnumMap;
import java.util.regex.Pattern;

/**
 * Command line wrapper for analyzing Blaze build profiles.
 */
@Command(name = "analyze-profile",
         options = { ProfileCommand.ProfileOptions.class },
         shortDescription = "Analyzes build profile data.",
         help = "resource:analyze-profile.txt",
         allowResidue = true,
         completion = "path",
         mustRunInWorkspace = false)
public final class ProfileCommand implements BlazeCommand {

  public static class DumpConverter extends Converters.StringSetConverter {
    public DumpConverter() {
      super("text", "raw", "text-unsorted", "raw-unsorted");
    }
  }

  public static class ProfileOptions extends OptionsBase {
    @Option(
      name = "chart",
      defaultValue = "true",
      help =
          "If --nochart is present, do not include the task chart with --html_details."
              + " The default is --chart."
    )
    public boolean chart;

    @Option(
      name = "combine",
      defaultValue = "null",
      help =
          "If present, the statistics of all given profile files will be combined and output"
              + " in text/--html format to the file named in the argument. Does not output HTML"
              + " task charts."
    )
    public String combine;

    @Option(name = "dump",
        abbrev='d',
        converter = DumpConverter.class,
        defaultValue = "null",
        help = "output full profile data dump either in human-readable 'text' format or"
            + " script-friendly 'raw' format, either sorted or unsorted.")
    public String dumpMode;

    @Option(name = "html",
        defaultValue = "false",
        help = "If present, an HTML file visualizing the tasks of the profiled build is created. "
            + "The name of the html file is the name of the profile file plus '.html'.")
    public boolean html;

    @Option(name = "html_pixels_per_second",
        defaultValue = "50",
        help = "Defines the scale of the time axis of the task diagram. The unit is "
            + "pixels per second. Default is 50 pixels per second. ")
    public int htmlPixelsPerSecond;

    @Option(
      name = "html_details",
      defaultValue = "false",
      help =
          "If --html_details is present, the task diagram contains all tasks of the profile "
              + " and performance statistics on user-defined and built-in Skylark functions. "
              + "If --nohtml_details is present, an aggregated diagram is generated. The default "
              + "is to generate an aggregated diagram."
    )
    public boolean htmlDetails;

    @Option(
      name = "html_histograms",
      defaultValue = "false",
      help =
          "If --html_histograms and --html_details is present, the HTML output will display"
              + " histograms for Skylark functions clicked in the statistics table. This will"
              + " increase file size massively."
    )
    public boolean htmlHistograms;

    @Option(
      name = "task_tree",
      defaultValue = "null",
      converter = Converters.RegexPatternConverter.class,
      help =
          "Print the tree of profiler tasks from all tasks matching the given regular expression."
    )
    public Pattern taskTree;

    @Option(
      name = "task_tree_threshold",
      defaultValue = "50",
      help =
          "When printing a task tree, will skip tasks with a duration that is less than the"
              + " given threshold in milliseconds."
    )
    public long taskTreeThreshold;

    @Option(name = "vfs_stats",
        defaultValue = "false",
        help = "If present, include VFS path statistics.")
    public boolean vfsStats;

    @Option(name = "vfs_stats_limit",
        defaultValue = "-1",
        help = "Maximum number of VFS path statistics to print.")
    public int vfsStatsLimit;
  }

  private InfoListener getInfoListener(final CommandEnvironment env) {
    return new InfoListener() {
      private final EventHandler reporter = env.getReporter();

      @Override
      public void info(String text) {
        reporter.handle(Event.info(text));
      }

      @Override
      public void warn(String text) {
        reporter.handle(Event.warn(text));
      }
    };
  }

  @Override
  public void editOptions(CommandEnvironment env, OptionsParser optionsParser) {}

  @Override
  public ExitCode exec(final CommandEnvironment env, OptionsProvider options) {
    ProfileOptions opts =
        options.getOptions(ProfileOptions.class);

    if (!opts.vfsStats) {
      opts.vfsStatsLimit = 0;
    }

    try (PrintStream out = new PrintStream(env.getReporter().getOutErr().getOutputStream())) {
      env.getReporter().handle(Event.warn(
          null, "This information is intended for consumption by Blaze developers"
              + " only, and may change at any time.  Script against it at your own risk"));
      if (opts.combine != null && opts.dumpMode == null) {
        MultiProfileStatistics statistics =
            new MultiProfileStatistics(
                env.getWorkingDirectory(),
                env.getWorkspaceName(),
                options.getResidue(),
                getInfoListener(env),
                opts.vfsStatsLimit > 0);
        Path outputFile = env.getWorkingDirectory().getRelative(opts.combine);
        try (PrintStream output =
                new PrintStream(new BufferedOutputStream(outputFile.getOutputStream()))) {
          if (opts.html) {
            env.getReporter().handle(Event.info("Creating HTML output in " + outputFile));
            HtmlCreator.create(
                output, statistics, opts.htmlDetails, opts.htmlPixelsPerSecond, opts.vfsStatsLimit);
          } else {
            env.getReporter().handle(Event.info("Creating text output in " + outputFile));
            new PhaseText(
                    output,
                    statistics.getSummaryStatistics(),
                    statistics.getSummaryPhaseStatistics(),
                    Optional.<CriticalPathStatistics>absent(),
                    statistics.getMissingActionsCount(),
                    opts.vfsStatsLimit)
                .print();
          }
        } catch (IOException e) {
          env
              .getReporter()
              .handle(
                  Event.error(
                      "Failed to write to output file " + outputFile + ":" + e.getMessage()));
        }
      } else {
        for (String name : options.getResidue()) {
          Path profileFile = env.getWorkingDirectory().getRelative(name);
          try {
            ProfileInfo info = ProfileInfo.loadProfileVerbosely(profileFile, getInfoListener(env));

            if (opts.dumpMode == null || !opts.dumpMode.contains("unsorted")) {
              ProfileInfo.aggregateProfile(info, getInfoListener(env));
            }

            if (opts.taskTree != null) {
              printTaskTree(out, name, info, opts.taskTree, opts.taskTreeThreshold);
              continue;
            }

            if (opts.dumpMode != null) {
              dumpProfile(info, out, opts.dumpMode);
              continue;
            }

            PhaseSummaryStatistics phaseSummaryStatistics = new PhaseSummaryStatistics(info);
            EnumMap<ProfilePhase, PhaseStatistics> phaseStatistics =
                new EnumMap<>(ProfilePhase.class);
            for (ProfilePhase phase : ProfilePhase.values()) {
              phaseStatistics.put(
                  phase,
                  new PhaseStatistics(
                      phase, info, env.getWorkspaceName(), opts.vfsStatsLimit > 0));
            }

            CriticalPathStatistics critPathStats = new CriticalPathStatistics(info);
            if (opts.html) {
              Path htmlFile =
                  profileFile.getParentDirectory().getChild(profileFile.getBaseName() + ".html");

              env.getReporter().handle(Event.info("Creating HTML output in " + htmlFile));

              HtmlCreator.create(
                  info,
                  htmlFile,
                  phaseSummaryStatistics,
                  phaseStatistics,
                  critPathStats,
                  info.getMissingActionsCount(),
                  opts.htmlDetails,
                  opts.htmlPixelsPerSecond,
                  opts.vfsStatsLimit,
                  opts.chart,
                  opts.htmlHistograms);
            } else {
              new PhaseText(
                      out,
                      phaseSummaryStatistics,
                      phaseStatistics,
                      Optional.of(critPathStats),
                      info.getMissingActionsCount(),
                      opts.vfsStatsLimit)
                  .print();
            }
          } catch (IOException e) {
            System.out.println(e);
            env
                .getReporter()
                .handle(Event.error("Failed to analyze profile file(s): " + e.getMessage()));
          }
        }
      }
    }
    return ExitCode.SUCCESS;
  }

  /**
   * Prints trees rooted at tasks with a description matching a pattern.
   * @see Task#printTaskTree(PrintStream, long)
   */
  private void printTaskTree(
      PrintStream out,
      String fileName,
      ProfileInfo info,
      Pattern taskPattern,
      long taskDurationThreshold) {
    Iterable<Task> tasks = info.findTasksByDescription(taskPattern);
    if (Iterables.isEmpty(tasks)) {
      out.printf("No tasks matching %s found in profile file %s.", taskPattern, fileName);
      out.println();
    } else {
      int skipped = 0;
      for (Task task : tasks) {
        if (!task.printTaskTree(out, taskDurationThreshold)) {
          skipped++;
        }
      }
      if (skipped > 0) {
        out.printf("Skipped %d matching task(s) below the duration threshold.", skipped);
      }
      out.println();
    }
  }

  /**
   * Dumps all tasks in the requested format.
   */
  private void dumpProfile(ProfileInfo info, PrintStream out, String dumpMode) {
    if (dumpMode.contains("raw")) {
      for (ProfileInfo.Task task : info.allTasksById) {
        dumpRaw(task, out);
      }
    } else if (dumpMode.contains("unsorted")) {
      for (ProfileInfo.Task task : info.allTasksById) {
        dumpTask(task, out, 0);
      }
    } else {
      for (ProfileInfo.Task task : info.rootTasksById) {
        dumpTask(task, out, 0);
      }
    }
  }

  /**
   * Dumps the task information and all subtasks.
   */
  private void dumpTask(ProfileInfo.Task task, PrintStream out, int indent) {
    StringBuilder builder =
        new StringBuilder(
            String.format(
                Joiner.on('\n')
                    .join(
                        "",
                        "%s %s",
                        "Thread: %-6d  Id: %-6d  Parent: %d",
                        "Start time: %-12s   Duration: %s"),
                task.type,
                task.getDescription(),
                task.threadId,
                task.id,
                task.parentId,
                TimeUtilities.prettyTime(task.startTime),
                TimeUtilities.prettyTime(task.durationNanos)));
    if (task.hasStats()) {
      builder.append("\n");
      ProfileInfo.AggregateAttr[] stats = task.getStatAttrArray();
      for (ProfilerTask type : ProfilerTask.values()) {
        ProfileInfo.AggregateAttr attr = stats[type.ordinal()];
        if (attr != null) {
          builder.append(type.toString().toLowerCase()).append("=(").
              append(attr.count).append(", ").
              append(TimeUtilities.prettyTime(attr.totalTime)).append(") ");
        }
      }
    }
    out.println(StringUtil.indent(builder.toString(), indent));
    for (ProfileInfo.Task subtask : task.subtasks) {
      dumpTask(subtask, out, indent + 1);
    }
  }

  private void dumpRaw(ProfileInfo.Task task, PrintStream out) {
    StringBuilder aggregateString = new StringBuilder();
    ProfileInfo.AggregateAttr[] stats = task.getStatAttrArray();
    for (ProfilerTask type : ProfilerTask.values()) {
      ProfileInfo.AggregateAttr attr = stats[type.ordinal()];
      if (attr != null) {
        aggregateString.append(type.toString().toLowerCase()).append(",").
            append(attr.count).append(",").append(attr.totalTime).append(" ");
      }
    }
    out.println(
        Joiner.on('|')
            .join(
                task.threadId,
                task.id,
                task.parentId,
                task.startTime,
                task.durationNanos,
                aggregateString.toString().trim(),
                task.type,
                task.getDescription()));
  }
}
