diff --git a/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy b/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy
index 2e8ee28acb26..351b5e7b821e 100644
--- a/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy
+++ b/build/groovy/org/jetbrains/intellij/build/BaseIdeaProperties.groovy
@@ -88,6 +88,7 @@ abstract class BaseIdeaProperties extends JetBrainsProductProperties {
"intellij.vcs.git.featuresTrainer",
"intellij.lombok",
"intellij.searchEverywhereMl",
+ "intellij.platform.tracing.ide",
KotlinPluginBuilder.MAIN_KOTLIN_PLUGIN_MODULE,
)
diff --git a/build/groovy/org/jetbrains/intellij/build/CommunityStandaloneJpsBuilder.groovy b/build/groovy/org/jetbrains/intellij/build/CommunityStandaloneJpsBuilder.groovy
index da4239c87b77..408b4b51da0a 100644
--- a/build/groovy/org/jetbrains/intellij/build/CommunityStandaloneJpsBuilder.groovy
+++ b/build/groovy/org/jetbrains/intellij/build/CommunityStandaloneJpsBuilder.groovy
@@ -31,6 +31,7 @@ final class CommunityStandaloneJpsBuilder {
module("intellij.platform.util.text.matching")
module("intellij.platform.util.base")
module("intellij.platform.util.xmlDom")
+ module("intellij.platform.tracing")
}
jar("jps-launcher.jar") {
diff --git a/intellij.idea.community.main.iml b/intellij.idea.community.main.iml
index 1325d82b27dd..5a73dc063cfc 100644
--- a/intellij.idea.community.main.iml
+++ b/intellij.idea.community.main.iml
@@ -160,5 +160,6 @@
+
\ No newline at end of file
diff --git a/java/compiler/impl/intellij.java.compiler.impl.iml b/java/compiler/impl/intellij.java.compiler.impl.iml
index 74d26f04823f..a809f0a7ae0d 100644
--- a/java/compiler/impl/intellij.java.compiler.impl.iml
+++ b/java/compiler/impl/intellij.java.compiler.impl.iml
@@ -41,6 +41,7 @@
+
diff --git a/jps/jps-builders/intellij.platform.jps.build.iml b/jps/jps-builders/intellij.platform.jps.build.iml
index 386bc475a216..162226cfbce2 100644
--- a/jps/jps-builders/intellij.platform.jps.build.iml
+++ b/jps/jps-builders/intellij.platform.jps.build.iml
@@ -53,5 +53,6 @@
+
\ No newline at end of file
diff --git a/jps/jps-builders/src/org/jetbrains/jps/cmdline/BuildSession.java b/jps/jps-builders/src/org/jetbrains/jps/cmdline/BuildSession.java
index 9c060fd00b19..7a491411545f 100644
--- a/jps/jps-builders/src/org/jetbrains/jps/cmdline/BuildSession.java
+++ b/jps/jps-builders/src/org/jetbrains/jps/cmdline/BuildSession.java
@@ -9,6 +9,7 @@ import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.tracing.Tracer;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.concurrency.SequentialTaskExecutor;
import com.intellij.util.io.DataOutputStream;
@@ -34,6 +35,8 @@ import org.jetbrains.jps.service.JpsServiceManager;
import org.jetbrains.jps.service.SharedThreadPool;
import java.io.*;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.Executor;
@@ -120,6 +123,19 @@ final class BuildSession implements Runnable, CanceledStatus {
LOG.debug(" preloadedData = " + myPreloadedData);
LOG.debug(" buildType = " + myBuildType);
}
+
+ try {
+ String tracingFile = System.getProperty("tracingFile");
+ if (tracingFile != null) {
+ LOG.debug("Tracing enabled, file: " + tracingFile);
+ Path tracingFilePath = Paths.get(tracingFile);
+ Tracer.runTracer(1, tracingFilePath, 1, e -> {
+ LOG.warn(e);
+ });
+ }
+ } catch (IOException e) {
+ LOG.warn(e);
+ }
}
private static @NonNls String showFirstItemIfAny(List list) {
diff --git a/jps/jps-builders/src/org/jetbrains/jps/cmdline/ClasspathBootstrap.java b/jps/jps-builders/src/org/jetbrains/jps/cmdline/ClasspathBootstrap.java
index caf518dc28f5..3dadf1cff6ab 100644
--- a/jps/jps-builders/src/org/jetbrains/jps/cmdline/ClasspathBootstrap.java
+++ b/jps/jps-builders/src/org/jetbrains/jps/cmdline/ClasspathBootstrap.java
@@ -8,6 +8,7 @@ import com.intellij.openapi.application.ClassPathUtil;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.tracing.Tracer;
import com.intellij.uiDesigner.compiler.AlienFormFileException;
import com.intellij.uiDesigner.core.GridConstraints;
import com.intellij.util.SystemProperties;
@@ -128,6 +129,7 @@ public final class ClasspathBootstrap {
addToClassPath(Gson.class, cp); // gson
addToClassPath(cp, ArtifactRepositoryManager.getClassesFromDependencies());
+ addToClassPath(Tracer.class, cp); // tracing infrastructure
try {
Class> cmdLineWrapper = Class.forName("com.intellij.rt.execution.CommandLineWrapper");
diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/JavaPluginLayout.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/JavaPluginLayout.groovy
index c903d52e0e52..a82282e4d587 100644
--- a/platform/build-scripts/groovy/org/jetbrains/intellij/build/JavaPluginLayout.groovy
+++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/JavaPluginLayout.groovy
@@ -15,6 +15,7 @@ final class JavaPluginLayout {
withModule("intellij.platform.jps.build.launcher", "jps-launcher.jar")
withModule("intellij.platform.jps.build", "jps-builders.jar")
withModule("intellij.platform.jps.build.javac.rt", "jps-builders-6.jar")
+ withModule("intellij.platform.tracing")
withModule("intellij.java.aetherDependencyResolver", "aether-dependency-resolver.jar")
withModule("intellij.java.jshell.protocol", "jshell-protocol.jar")
withModule("intellij.java.resources", mainJarName)
diff --git a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/PlatformModules.groovy b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/PlatformModules.groovy
index e4e11ec8ed4d..b371b5a5b6e3 100644
--- a/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/PlatformModules.groovy
+++ b/platform/build-scripts/groovy/org/jetbrains/intellij/build/impl/PlatformModules.groovy
@@ -103,6 +103,7 @@ final class PlatformModules {
"intellij.platform.credentialStore.ui",
"intellij.platform.rd.community",
"intellij.platform.ml.impl",
+ "intellij.platform.tracing",
)
private static final String UTIL_JAR = "util.jar"
diff --git a/platform/lang-impl/intellij.platform.lang.impl.iml b/platform/lang-impl/intellij.platform.lang.impl.iml
index a54e9af16d24..7b4f9ee6650a 100644
--- a/platform/lang-impl/intellij.platform.lang.impl.iml
+++ b/platform/lang-impl/intellij.platform.lang.impl.iml
@@ -66,5 +66,6 @@
+
\ No newline at end of file
diff --git a/platform/tracing-ide/intellij.platform.tracing.ide.iml b/platform/tracing-ide/intellij.platform.tracing.ide.iml
new file mode 100644
index 000000000000..901546400c50
--- /dev/null
+++ b/platform/tracing-ide/intellij.platform.tracing.ide.iml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/tracing-ide/resources/META-INF/plugin.xml b/platform/tracing-ide/resources/META-INF/plugin.xml
new file mode 100644
index 000000000000..1f43a5ffe7fa
--- /dev/null
+++ b/platform/tracing-ide/resources/META-INF/plugin.xml
@@ -0,0 +1,24 @@
+
+ com.intellij.tracing.ide
+ JetBrains
+
+ com.intellij.modules.java
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ messages.TracingBundle
+
\ No newline at end of file
diff --git a/platform/tracing-ide/resources/messages/TracingBundle.properties b/platform/tracing-ide/resources/messages/TracingBundle.properties
new file mode 100644
index 000000000000..6683f8b538c7
--- /dev/null
+++ b/platform/tracing-ide/resources/messages/TracingBundle.properties
@@ -0,0 +1,5 @@
+action.open.trace.directory.in.file.manager.text=Open Directory in File Manager
+action.toggle.build.tracing.text=Toggle Build Tracing
+action.toggle.tracing.action.description=Toggle tracing
+build.tracing.group=Build tracing
+notification.content.tracing.file.was.created=Tracing file was created
\ No newline at end of file
diff --git a/platform/tracing-ide/src/com/intellij/tracing/ide/ToggleBuildTracingAction.kt b/platform/tracing-ide/src/com/intellij/tracing/ide/ToggleBuildTracingAction.kt
new file mode 100644
index 000000000000..b7954b78b229
--- /dev/null
+++ b/platform/tracing-ide/src/com/intellij/tracing/ide/ToggleBuildTracingAction.kt
@@ -0,0 +1,15 @@
+// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.tracing.ide
+
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.actionSystem.ToggleAction
+
+class ToggleBuildTracingAction : ToggleAction(TracingBundle.message("action.toggle.build.tracing.text")) {
+ override fun isSelected(e: AnActionEvent): Boolean {
+ return TracingService.getInstance().isTracingEnabled()
+ }
+
+ override fun setSelected(e: AnActionEvent, state: Boolean) {
+ TracingService.getInstance().setTracingEnabled(state)
+ }
+}
\ No newline at end of file
diff --git a/platform/tracing-ide/src/com/intellij/tracing/ide/TracingBuildProcessParameterProvider.kt b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingBuildProcessParameterProvider.kt
new file mode 100644
index 000000000000..6ecb13f378ac
--- /dev/null
+++ b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingBuildProcessParameterProvider.kt
@@ -0,0 +1,15 @@
+// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.tracing.ide
+
+import com.intellij.compiler.server.BuildProcessParametersProvider
+
+internal class TracingBuildProcessParameterProvider : BuildProcessParametersProvider() {
+ override fun getVMArguments(): List {
+ if (TracingService.getInstance().isTracingEnabled()) {
+ val path = TracingService.createPath(TracingService.TraceKind.Jps)
+ TracingService.getInstance().registerJpsTrace(path)
+ return listOf("-DtracingFile=$path")
+ }
+ return emptyList()
+ }
+}
\ No newline at end of file
diff --git a/platform/tracing-ide/src/com/intellij/tracing/ide/TracingBundle.kt b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingBundle.kt
new file mode 100644
index 000000000000..ce8af2c5f863
--- /dev/null
+++ b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingBundle.kt
@@ -0,0 +1,23 @@
+// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.tracing.ide
+
+import com.intellij.DynamicBundle
+import org.jetbrains.annotations.Nls
+import org.jetbrains.annotations.NonNls
+import org.jetbrains.annotations.PropertyKey
+
+internal class TracingBundle : DynamicBundle(BUNDLE) {
+ companion object {
+ @NonNls
+ private const val BUNDLE = "messages.TracingBundle"
+
+ @JvmStatic
+ private val INSTANCE: TracingBundle = TracingBundle()
+
+ @JvmStatic
+ @Nls
+ fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String {
+ return INSTANCE.getMessage(key, *params)
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/tracing-ide/src/com/intellij/tracing/ide/TracingPersistentStateComponent.kt b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingPersistentStateComponent.kt
new file mode 100644
index 000000000000..59af1cf85cad
--- /dev/null
+++ b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingPersistentStateComponent.kt
@@ -0,0 +1,16 @@
+// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.tracing.ide
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.*
+
+@State(name = "tracing", storages = [(Storage(value = "tracing.xml"))])
+internal class TracingPersistentStateComponent : SimplePersistentStateComponent(State()) {
+ companion object {
+ fun getInstance(): TracingPersistentStateComponent = ApplicationManager.getApplication().getService(TracingPersistentStateComponent::class.java)
+ }
+
+ class State : BaseState() {
+ var isEnabled by property(false)
+ }
+}
\ No newline at end of file
diff --git a/platform/tracing-ide/src/com/intellij/tracing/ide/TracingProjectTaskListener.kt b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingProjectTaskListener.kt
new file mode 100644
index 000000000000..9154eef2089d
--- /dev/null
+++ b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingProjectTaskListener.kt
@@ -0,0 +1,99 @@
+// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.tracing.ide
+
+import com.intellij.ide.util.PsiNavigationSupport
+import com.intellij.notification.Notification
+import com.intellij.notification.NotificationType
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.task.ProjectTaskContext
+import com.intellij.task.ProjectTaskListener
+import com.intellij.task.ProjectTaskManager
+import com.intellij.tracing.Tracer
+import com.intellij.util.concurrency.AppExecutorUtil
+import com.intellij.util.io.exists
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.writeText
+import kotlin.jvm.Throws
+
+internal class TracingProjectTaskListener : ProjectTaskListener {
+ companion object {
+ private val log = logger()
+ }
+
+ @Volatile
+ private var span: Tracer.Span? = null
+
+ override fun started(context: ProjectTaskContext) {
+ val tracingService = TracingService.getInstance()
+ if (!tracingService.isTracingEnabled()) return
+ try {
+ val filePath = TracingService.createPath(TracingService.TraceKind.Ide)
+ tracingService.registerIdeTrace(filePath)
+ tracingService.bindJpsTraceIfExistsToCurrentSession()
+ Tracer.runTracer(0, filePath, 1) { exception ->
+ handleException(tracingService, exception)
+ }
+ span = Tracer.start("Build")
+ } catch (e: IOException) {
+ handleException(tracingService, e)
+ }
+ }
+
+ override fun finished(result: ProjectTaskManager.Result) {
+ span?.complete()
+ val tracingService = TracingService.getInstance()
+ Tracer.finishTracer { exception ->
+ handleException(tracingService, exception)
+ }
+ val filesToMerge = tracingService.drainFilesToMerge()
+ AppExecutorUtil.getAppExecutorService().execute {
+ try {
+ val mergedText = mergeFiles(filesToMerge)
+ val mergedFilePath = TracingService.createPath(TracingService.TraceKind.Merged)
+ Files.createDirectories(mergedFilePath.parent)
+ mergedFilePath.writeText(mergedText)
+ showNotificationNotification(mergedFilePath.parent)
+ }
+ catch (e: IOException) {
+ handleException(tracingService, e)
+ }
+ }
+ }
+
+ private fun handleException(tracingService: TracingService, e: Exception) {
+ tracingService.clearPathsToMerge()
+ log.warn(e)
+ }
+
+ private fun showNotificationNotification(mergedFile: Path) {
+ val notification = Notification("BuildTracing", TracingBundle.message("notification.content.tracing.file.was.created"), NotificationType.INFORMATION)
+ notification.addAction(object : AnAction(TracingBundle.message("action.open.trace.directory.in.file.manager.text")) {
+ override fun actionPerformed(e: AnActionEvent) {
+ PsiNavigationSupport.getInstance().openDirectoryInSystemFileManager(mergedFile.parent.toFile())
+ }
+ })
+ notification.notify(null)
+ }
+
+ @Throws(IOException::class)
+ fun mergeFiles(files: List) : String {
+ return buildString {
+ appendLine("[\n")
+ for (filePath in files) {
+ if (filePath.exists()) {
+ val entries = readEntries(filePath)
+ for (entry in entries) {
+ appendLine(entry)
+ }
+ }
+ }
+ appendLine("]\n")
+ }
+ }
+
+ private fun readEntries(trace: Path) = trace.toFile().bufferedReader().lineSequence().drop(1).toList().dropLast(1)
+}
\ No newline at end of file
diff --git a/platform/tracing-ide/src/com/intellij/tracing/ide/TracingService.kt b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingService.kt
new file mode 100644
index 000000000000..9d0abbc0f61e
--- /dev/null
+++ b/platform/tracing-ide/src/com/intellij/tracing/ide/TracingService.kt
@@ -0,0 +1,103 @@
+// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.tracing.ide
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.PathManager
+import com.intellij.openapi.components.Service
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+import java.util.*
+
+@Service(Service.Level.APP)
+internal class TracingService {
+ companion object {
+ fun getInstance() : TracingService {
+ return ApplicationManager.getApplication().getService(TracingService::class.java)
+ }
+
+ fun createPath(kind: TraceKind): Path {
+ val tracesDirPath = getTracesDirPath()
+ val subDir = when (kind) {
+ TraceKind.Jps -> JPS_TRACE_DIR_NAME
+ TraceKind.Ide -> IDE_TRACE_DIR_NAME
+ TraceKind.Merged -> MERGED_TRACE_DIR_NAME
+ }
+ return tracesDirPath.resolve(subDir).resolve(getNewTraceFileName())
+ }
+
+ private fun getNewTraceFileName() = "trace_" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + ".json"
+
+ private fun getTracesDirPath() : Path {
+ return Paths.get(PathManager.getHomePath()).resolve(COMMON_TRACING_DIR_NAME)
+ }
+
+ private const val COMMON_TRACING_DIR_NAME: String = "tracing"
+ private const val IDE_TRACE_DIR_NAME: String = "ide"
+ private const val JPS_TRACE_DIR_NAME: String = "jps"
+ private const val MERGED_TRACE_DIR_NAME: String = "merged"
+ }
+
+ private val lock = Any()
+ private var traces = ArrayList()
+ private var jpsTrace: Path? = null
+
+ fun isTracingEnabled() : Boolean {
+ return synchronized(lock) {
+ TracingPersistentStateComponent.getInstance().state.isEnabled
+ }
+ }
+
+ fun setTracingEnabled(enabled: Boolean) {
+ synchronized(lock) {
+ TracingPersistentStateComponent.getInstance().state.isEnabled = enabled
+ if (!enabled) {
+ traces.clear()
+ }
+ }
+ }
+
+ fun registerIdeTrace(filePath: Path) {
+ synchronized(lock) {
+ traces.add(filePath)
+ }
+ }
+
+ fun registerJpsTrace(filePath: Path) {
+ synchronized(lock) {
+ bindJpsTraceIfExistsToCurrentSession()
+ jpsTrace = filePath
+ }
+ }
+
+ fun bindJpsTraceIfExistsToCurrentSession() {
+ synchronized(lock) {
+ val jpsTrace = jpsTrace
+ if (jpsTrace != null) {
+ traces.add(jpsTrace)
+ this.jpsTrace = null
+ }
+ }
+ }
+
+ fun drainFilesToMerge() : List {
+ synchronized(lock) {
+ val tracesToMerge = traces
+ traces = ArrayList()
+ return tracesToMerge
+ }
+ }
+
+ fun clearPathsToMerge() {
+ synchronized(lock) {
+ traces.clear()
+ }
+ }
+
+ enum class TraceKind {
+ Jps,
+ Ide,
+ Merged,
+ }
+}
\ No newline at end of file
diff --git a/platform/tracing/intellij.platform.tracing.iml b/platform/tracing/intellij.platform.tracing.iml
new file mode 100644
index 000000000000..c90834f2d607
--- /dev/null
+++ b/platform/tracing/intellij.platform.tracing.iml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/tracing/src/com/intellij/tracing/Tracer.java b/platform/tracing/src/com/intellij/tracing/Tracer.java
new file mode 100644
index 000000000000..007d25612abb
--- /dev/null
+++ b/platform/tracing/src/com/intellij/tracing/Tracer.java
@@ -0,0 +1,211 @@
+// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.tracing;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class Tracer {
+ private static long tracingStartNs;
+ private static long tracingStartMs;
+ private static final AtomicLong eventId = new AtomicLong();
+ private static final ConcurrentLinkedDeque spans = new ConcurrentLinkedDeque<>();
+ private static volatile int pid;
+ private static volatile long durationThreshold;
+ private static volatile FileState fileState = null;
+ private static volatile boolean running = false;
+ private static ScheduledExecutorService executor = null;
+ private static Thread shutdownHook;
+
+ private Tracer() { }
+
+
+ public static DelayedSpan start(Supplier nameSupplier) {
+ long eventId = Tracer.eventId.getAndIncrement();
+ long threadId = Thread.currentThread().getId();
+ long startNs = System.nanoTime();
+ return new DelayedSpan(eventId, threadId, nameSupplier, startNs);
+ }
+
+ public static Span start(String name) {
+ long eventId = Tracer.eventId.getAndIncrement();
+ long threadId = Thread.currentThread().getId();
+ long startNs = System.nanoTime();
+ return new Span(eventId, threadId, name, startNs);
+ }
+
+ public static void runTracer(int pid, Path filePath, long threshold, Consumer exceptionHandler) throws IOException {
+ if (running) throw new IllegalStateException("Tracer already started");
+ tracingStartMs = System.currentTimeMillis();
+ tracingStartNs = System.nanoTime();
+ Files.createDirectories(filePath.getParent());
+ FileOutputStream fileOutputStream = new FileOutputStream(filePath.toFile());
+ OutputStreamWriter writer = new OutputStreamWriter(new BufferedOutputStream(fileOutputStream), StandardCharsets.UTF_8);
+ fileState = new FileState(writer);
+ durationThreshold = threshold;
+ Tracer.pid = pid;
+ executor = createExecutor();
+ FlushingTask flushingTask = new FlushingTask(fileState, false, exceptionHandler);
+ executor.scheduleAtFixedRate(flushingTask, 5, 5, TimeUnit.SECONDS);
+ shutdownHook = new Thread(new FlushingTask(fileState, true, exceptionHandler), "Shutdown hook trace flusher");
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ running = true;
+ }
+
+ public static void finishTracer(Consumer exceptionHandler) {
+ if (!running) throw new IllegalStateException("Tracer already finished");
+ new FlushingTask(fileState, true, exceptionHandler).run();
+ fileState = null;
+ executor.shutdown();
+ executor = null;
+ Runtime.getRuntime().removeShutdownHook(shutdownHook);
+ running = false;
+ }
+
+ public static boolean isRunning() {
+ return running;
+ }
+
+ private static ScheduledExecutorService createExecutor() {
+ return Executors.newScheduledThreadPool(1, r -> {
+ Thread thread = new Thread(r, "Trace flusher");
+ thread.setDaemon(true);
+ return thread;
+ });
+ }
+
+ public static class DelayedSpan {
+ final long eventId;
+ final long threadId;
+ final Supplier nameSupplier;
+ final long startTimeNs;
+
+ public DelayedSpan(long eventId, long threadId, Supplier nameSupplier, long startTimeNs) {
+ this.eventId = eventId;
+ this.threadId = threadId;
+ this.nameSupplier = nameSupplier;
+ this.startTimeNs = startTimeNs;
+ }
+
+ public void complete() {
+ if (running) {
+ Span span = new Span(eventId, threadId, nameSupplier.get(), startTimeNs);
+ span.complete();
+ }
+ }
+ }
+
+ public static class Span {
+ final long eventId;
+ final long threadId;
+ final String name;
+ final long startTimeNs;
+ long finishTimeNs;
+
+ public Span(long eventId, long threadId, String name, long startTimeNs) {
+ this.eventId = eventId;
+ this.threadId = threadId;
+ this.name = name;
+ this.startTimeNs = startTimeNs;
+ }
+
+ public void complete() {
+ if (running) {
+ finishTimeNs = System.nanoTime();
+ if (getDuration() > durationThreshold) {
+ spans.offerLast(this);
+ }
+ }
+ }
+
+ /**
+ * If event has been started on one thread and finished on the other it is not guaranteed to have non-negative duration
+ */
+ long getDuration() {
+ return finishTimeNs - startTimeNs;
+ }
+ }
+
+ private static class FileState {
+ final Writer writer;
+ boolean openBracketWritten = false;
+ boolean finished = false;
+
+ private FileState(Writer writer) {
+ this.writer = writer;
+ }
+ }
+
+ private static class FlushingTask implements Runnable {
+ private final FileState fileState;
+ private final boolean shouldFinish;
+ private final Consumer myExceptionHandler;
+
+ private FlushingTask(FileState fileState, boolean shouldFinish, Consumer exceptionHandler) {
+ this.fileState = fileState;
+ this.shouldFinish = shouldFinish;
+ myExceptionHandler = exceptionHandler;
+ }
+
+ @Override
+ public void run() {
+ synchronized (fileState) {
+ if (fileState.finished) return;
+ try {
+ if (!fileState.openBracketWritten) {
+ fileState.writer.write("[\n");
+ fileState.openBracketWritten = true;
+ }
+ while (true) {
+ Span span = spans.pollLast();
+ if (span == null) break;
+ fileState.writer.write(serialize(span, true));
+ fileState.writer.write(serialize(span, false));
+ }
+ if (shouldFinish) {
+ fileState.writer.write("]");
+ }
+ fileState.writer.flush();
+ }
+ catch (IOException e) {
+ myExceptionHandler.accept(e);
+ }
+ }
+ }
+
+ private static String serialize(Span span, boolean isStart) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{\"name\": \"")
+ .append(span.name)
+ .append("\", \"cat\": \"PERF\", \"ph\": ");
+ if (isStart) {
+ sb.append("\"B\"");
+ }
+ else {
+ sb.append("\"E\"");
+ }
+ sb.append(", \"pid\": ").append(pid)
+ .append(", \"tid\": ").append(span.threadId)
+ .append(", \"ts\": ");
+ if (isStart) {
+ sb.append(getTimeUs(span.startTimeNs));
+ }
+ else {
+ sb.append(getTimeUs(span.finishTimeNs));
+ }
+ return sb.append("},\n").toString();
+ }
+ }
+
+ static long getTimeUs(long timeNs) {
+ return (tracingStartMs * 1000_000 - tracingStartNs + timeNs) / 1000;
+ }
+}