// Copyright 2016 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package com.twitter.intellij.pants.metrics;
import com.google.common.base.Stopwatch;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.DumbServiceImpl;
import com.intellij.openapi.project.Project;
import com.twitter.intellij.pants.util.PantsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* This class keeps track of the metrics globally.
* TODO: keep the metrics per project.
*/
public class PantsMetrics {
public static ScheduledExecutorService indexThreadPool;
private static ConcurrentHashMap<String, Stopwatch> timers = new ConcurrentHashMap<>();
public static final String SYSTEM_PROPERTY_METRICS_REPORT_DIR = "pants.metrics.report.dir";
public static final String SYSTEM_PROPERTY_METRICS_IMPORT_DIR = "pants.metrics.import.dir";
public static final String SYSTEM_PROPERTY_METRICS_ENABLE = "pants.metrics.enable";
private static volatile ScheduledFuture handle;
private static volatile int counter = 0;
private static final String METRIC_INDEXING = "indexing_second";
private static final String METRIC_LOAD = "load_second";
private static final String METRIC_EXPORT = "export_second";
@Nullable
public static String getMetricsImportDir() {
return System.getProperty(SYSTEM_PROPERTY_METRICS_IMPORT_DIR);
}
public static void setMetricsImportDir(String dir) {
System.setProperty(SYSTEM_PROPERTY_METRICS_IMPORT_DIR, dir);
}
@Nullable
public static String getMetricsReportDir() {
return System.getProperty(SYSTEM_PROPERTY_METRICS_REPORT_DIR);
}
public static void setMetricsReportDir(String dir) {
System.setProperty(SYSTEM_PROPERTY_METRICS_REPORT_DIR, dir);
}
/**
* This starts the indexing timer when certain conditions are met,
* because the factor to determine whether indexing has started is
* different in unit test and in GUI mode.
*
* @param myProject current Project.
*/
public static void prepareTimeIndexing(Project myProject) {
if (!isMetricsEnabled()) {
return;
}
if (ApplicationManager.getApplication().isUnitTestMode()) {
markIndexStart();
return;
}
/**
* This portion only applies in manual testing.
*/
handle = indexThreadPool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
// Start counting now in unit test mode, because dumb mode is never set.
if (!DumbServiceImpl.getInstance(myProject).isDumb()) {
return;
}
// Still in smart mode, meaning indexing hasn't started yet.
counter++;
if (counter > 10) {
handle.cancel(false);
counter = 0;
}
markIndexStart();
DumbServiceImpl.getInstance(myProject).runWhenSmart(new Runnable() {
@Override
public void run() {
markIndexEnd();
report();
}
});
handle.cancel(false);
}
}, 0, 1, TimeUnit.SECONDS);
}
public static void initialize() {
timers.put(METRIC_EXPORT, Stopwatch.createUnstarted());
timers.put(METRIC_LOAD, Stopwatch.createUnstarted());
timers.put(METRIC_INDEXING, Stopwatch.createUnstarted());
// Thread related things.
if (indexThreadPool != null && !indexThreadPool.isShutdown()) {
indexThreadPool.shutdown();
}
indexThreadPool = Executors.newSingleThreadScheduledExecutor(
new ThreadFactory() {
@Override
public Thread newThread(@NotNull Runnable r) {
return new Thread(r, "Pants-Plugin-Metrics-Pool");
}
});
}
public static void markResolveStart() {
startWatch(timers.get(METRIC_LOAD));
}
public static void markResolveEnd() {
stopWatch(timers.get(METRIC_LOAD));
}
public static void markExportStart() {
startWatch(timers.get(METRIC_EXPORT));
}
public static void markExportEnd() {
stopWatch(timers.get(METRIC_EXPORT));
}
public static void markIndexStart() {
startWatch(timers.get(METRIC_INDEXING));
}
public static void markIndexEnd() {
stopWatch(timers.get(METRIC_INDEXING));
}
public static void report() {
if (!isMetricsEnabled()) {
return;
}
Map<String, Long> report = getCurrentResult();
System.out.println(report);
String reportFilePath = getReportFilePath();
if (reportFilePath == null) {
return;
}
try (Writer writer = new FileWriter(reportFilePath)) {
PantsUtil.gson.toJson(report, writer);
}
catch (IOException e) {
e.printStackTrace();
}
}
public static Map<String, Long> getCurrentResult() {
return timers.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().elapsed(TimeUnit.SECONDS)));
}
@Nullable
public static String getReportFilePath() {
String reportDir = getMetricsReportDir();
if (reportDir == null) {
return null;
}
return getMetricsReportDir() + "/output.json";
}
private static void startWatch(Stopwatch stopwatch) {
if (!isMetricsEnabled()) {
return;
}
stopwatch.start();
}
private static void stopWatch(Stopwatch stopwatch) {
if (!isMetricsEnabled()) {
return;
}
stopwatch.stop();
}
private static boolean isMetricsEnabled() {
String property = System.getProperty(SYSTEM_PROPERTY_METRICS_ENABLE);
return property != null && property.equals("true");
}
}