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; + } +}