blob: d5af3c07bbe55a762ac5df25dd2baa2ff51393f2 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* 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.android.tools.idea.diagnostics;
import static java.nio.file.Files.newDirectoryStream;
import com.android.annotations.NonNull;
import com.android.tools.idea.diagnostics.report.DiagnosticReport;
import com.intellij.diagnostic.IdePerformanceListener;
import com.intellij.diagnostic.ThreadDump;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.messages.MessageBusConnection;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
class DiagnosticReportIdePerformanceListener implements IdePerformanceListener {
private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.diagnostics.DiagnosticReportIdePerformanceListener");
private final Consumer<DiagnosticReport> myReportCallback;
private @Nullable DiagnosticReportBuilder myBuilder;
private @Nullable ReportContext myContext;
private int myReportsCollected;
private @Nullable MessageBusConnection myMessageBusConnection;
private @NonNull LastActionTracker myLastActionTracker;
private static class ReportContext {
private Path myThreadDumpPath;
}
public DiagnosticReportIdePerformanceListener(Consumer<DiagnosticReport> reportCallback) {
myReportCallback = reportCallback;
myLastActionTracker = new LastActionTracker();
}
@Override
public void dumpedThreads(@NotNull File toFile, @NotNull ThreadDump dump) {
ReportContext currentContext = myContext;
if (currentContext != null && currentContext.myThreadDumpPath == null) {
currentContext.myThreadDumpPath = toFile.toPath().getParent();
}
}
@Override
public void uiFreezeStarted() {
LOG.info("uiFreezeStarted");
if (myBuilder != null) {
return;
}
final ReportContext context = new ReportContext();
myContext = context;
// Unfortunately, current API does not give us exact value how long the UI was frozen before uiFreezeStarted()
// event is triggered. This is the best approximation of that value.
final int freezeTimeBeforeCreatedMs = Registry.intValue("performance.watcher.unresponsive.interval.ms");
myBuilder = new DiagnosticReportBuilder(
DiagnosticReportBuilder.INTERVAL_MS,
DiagnosticReportBuilder.MAX_DURATION_MS,
DiagnosticReportBuilder.FRAME_IGNORE_THRESHOLD_MS,
freezeTimeBeforeCreatedMs,
myLastActionTracker,
new Controller(context)
);
}
private void reportReady(DiagnosticReport report) {
myReportCallback.accept(report);
}
@Override
public void uiFreezeFinished(long durationMs, @Nullable File reportDir) {
int lengthInSeconds = (int)(durationMs / 1000);
LOG.info(String.format(Locale.US, "uiFreezeFinished: duration = %d seconds", lengthInSeconds));
DiagnosticReportBuilder localBuilder = myBuilder;
if (localBuilder == null) {
return;
}
try {
myReportsCollected++;
if (DiagnosticReportBuilder.MAX_REPORTS != -1 && myReportsCollected >= DiagnosticReportBuilder.MAX_REPORTS) {
LOG.info("Stopped collecting UI freeze reports after " + myReportsCollected + " reports.");
unregister();
}
if (myContext != null) {
if (myContext.myThreadDumpPath == null) {
Path directoryForFreeze = tryCreateDirectoryForFreeze(lengthInSeconds);
if (directoryForFreeze == null) {
return;
}
myContext.myThreadDumpPath = directoryForFreeze;
}
Path localReportPath = getPathForReportName("profileDiagnostics", myContext);
// If the report has already been generated (freeze took too long), append a line with a real
// freeze duration.
if (localReportPath != null) {
if (Files.exists(localReportPath)) {
try {
Files.write(localReportPath, ("UI freeze lasted " + lengthInSeconds + " seconds.\n").getBytes(), StandardOpenOption.APPEND);
}
catch (IOException e) {
// Non fatal exception
LOG.warn("Exception while appending to a report.", e);
}
}
}
}
} finally {
myBuilder = null;
myContext = null;
localBuilder.stop();
}
}
private static Path tryCreateDirectoryForFreeze(long freezeInSeconds) {
String dirName = "uiFreeze-" + formatTime(System.currentTimeMillis()) + "-" + freezeInSeconds + "sec";
Path freezeDir = Paths.get(PathManager.getLogPath(), dirName);
try {
if (Files.exists(freezeDir)) {
return null;
}
Files.createDirectories(freezeDir);
return freezeDir;
}
catch (IOException e) {
return null;
}
}
private static String formatTime(long timeMs) {
return new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US).format(new Date(timeMs));
}
@NonNull
private static Path tryFixReportPath(@NotNull Path path) {
Path reportDirectory = path.getParent();
if (Files.isDirectory(reportDirectory)) {
return path;
}
final String directoryGlob = reportDirectory.getFileName().toString() + "-*";
try (final DirectoryStream<Path> paths = newDirectoryStream(reportDirectory.getParent(), directoryGlob)) {
final Iterator<Path> iterator = paths.iterator();
if (iterator.hasNext()) {
reportDirectory = iterator.next();
}
}
catch (IOException e) {
LOG.warn(e);
}
if (reportDirectory == null) {
// No directory to store the report, return original value
return path;
}
return reportDirectory.resolve(path.getFileName());
}
@Nullable
private static Path getPathForReportName(@NotNull String reportName,
@NotNull ReportContext context) {
Path threadDumpPath = context.myThreadDumpPath;
if (threadDumpPath == null) {
return null;
}
Path reportPath = threadDumpPath.resolve("diagnosticReport-" + reportName + ".txt");
return tryFixReportPath(reportPath);
}
/**
* Save report to a file.
*
* @return Path to a report file or {@code null} if report could not be saved.
*/
@Nullable
private static Path saveReportFile(@NotNull String reportName,
@NotNull String reportContents,
ReportContext context) {
Path reportPath = getPathForReportName(reportName, context);
if (reportPath == null) {
return null;
}
if (Files.exists(reportPath)) {
return reportPath;
}
try (PrintWriter out = new PrintWriter(reportPath.toFile(), "UTF-8")) {
out.write(reportContents);
LOG.info(String.format("Freeze report saved: %s", reportPath));
}
catch (IOException e) {
LOG.warn(e);
}
return reportPath;
}
public void registerOn(Application application) {
assert myMessageBusConnection == null;
myMessageBusConnection = application.getMessageBus().connect(application);
myMessageBusConnection.subscribe(IdePerformanceListener.TOPIC, this);
}
public void unregister() {
assert myMessageBusConnection != null;
myMessageBusConnection.disconnect();
myMessageBusConnection = null;
Disposer.dispose(myLastActionTracker);
//noinspection ConstantConditions
myLastActionTracker = null;
}
public class Controller {
private final ReportContext myContext;
public Controller(ReportContext context) {
myContext = context;
}
public Path saveReportFile(String reportName, String reportContents) {
return DiagnosticReportIdePerformanceListener.saveReportFile(reportName, reportContents, myContext);
}
public void reportReady(DiagnosticReport report) {
DiagnosticReportIdePerformanceListener.this.reportReady(report);
}
}
}