/* * Copyright 2011-2017 the original author or authors. * * 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 org.glowroot.agent.impl; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.common.base.Ticker; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import com.google.common.collect.TreeMultimap; import com.google.common.io.BaseEncoding; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.glowroot.agent.collector.Collector.EntryVisitor; import org.glowroot.agent.config.AdvancedConfig; import org.glowroot.agent.config.ConfigService; import org.glowroot.agent.impl.TransactionCollection.TransactionEntry; import org.glowroot.agent.model.AsyncTimerImpl; import org.glowroot.agent.model.CommonTimerImpl; import org.glowroot.agent.model.ErrorMessage; import org.glowroot.agent.model.ImmutableTimerImplSnapshot; import org.glowroot.agent.model.MutableAggregateTimer; import org.glowroot.agent.model.MutableTraceTimer; import org.glowroot.agent.model.Profile; import org.glowroot.agent.model.QueryCollector; import org.glowroot.agent.model.ThreadStats; import org.glowroot.agent.model.TimerNameImpl; import org.glowroot.agent.plugin.api.Message; import org.glowroot.agent.plugin.api.MessageSupplier; import org.glowroot.agent.plugin.api.TimerName; import org.glowroot.agent.plugin.api.internal.ReadableMessage; import org.glowroot.agent.plugin.api.util.FastThreadLocal.Holder; import org.glowroot.agent.util.ThreadAllocatedBytes; import org.glowroot.common.model.ServiceCallCollector; import org.glowroot.common.util.Cancellable; import org.glowroot.common.util.NotAvailableAware; import org.glowroot.common.util.Traverser; import static com.google.common.base.Preconditions.checkNotNull; import static org.glowroot.agent.util.Checkers.castInitialized; // contains all data that has been captured for a given transaction (e.g. a servlet request) // // this class needs to be thread safe, only one thread updates it, but multiple threads can read it // at the same time as it is being updated public class Transaction { private static final Logger logger = LoggerFactory.getLogger(Transaction.class); static final int USE_GENERAL_STORE_THRESHOLD = -1; static final String AUXILIARY_THREAD_MESSAGE = "auxiliary thread"; // initial capacity is very important, see ThreadSafeCollectionOfTenBenchmark private static final int ATTRIBUTE_KEYS_INITIAL_CAPACITY = 16; // this is just to limit memory (and also to limit display size of trace) private static final long ATTRIBUTE_VALUES_PER_KEY_LIMIT = 1000; // this is only to limit memory private static final int TRANSACTION_AUX_THREAD_CONTEXT_LIMIT = Integer.getInteger("glowroot.transaction.aux.thread.context.limit", 10000); private static final Random random = new Random(); private volatile @Nullable String traceId; private final long startTime; private final long startTick; private volatile boolean async; private volatile boolean outer; private volatile String transactionType; private volatile int transactionTypePriority = Integer.MIN_VALUE; private volatile String transactionName; private volatile int transactionNamePriority = Integer.MIN_VALUE; private volatile @Nullable String user; private volatile int userPriority = Integer.MIN_VALUE; // lazy loaded to reduce memory when custom attributes are not used @GuardedBy("attributes") private volatile @MonotonicNonNull SetMultimap<String, String> attributes; // trace-level error private volatile @Nullable ErrorMessage errorMessage; private final int maxTraceEntriesPerTransaction; private final int maxAggregateQueriesPerType; private final int maxAggregateServiceCallsPerType; private final TransactionRegistry transactionRegistry; private final TransactionServiceImpl transactionService; private final ConfigService configService; // stack trace data constructed from profiling private volatile @MonotonicNonNull Profile mainThreadProfile; private volatile @MonotonicNonNull Profile auxThreadProfile; // overrides general store threshold // -1 means don't override the general store threshold private volatile int slowThresholdMillis = USE_GENERAL_STORE_THRESHOLD; private volatile int slowThresholdMillisPriority = Integer.MIN_VALUE; // these are stored in the trace so they are only scheduled a single time, and also so they can // be canceled at trace completion private volatile @MonotonicNonNull Cancellable userProfileRunnable; private volatile @MonotonicNonNull Cancellable immedateTraceStoreRunnable; private volatile boolean partiallyStored; private long captureTime; // memory barrier is used to ensure memory visibility of entries and timers at key points, // namely after each entry // // benchmarking shows this is significantly faster than ensuring memory visibility of each // timer update, the down side is that the latest updates to timers for transactions // that are captured in-flight (e.g. partial traces and active traces displayed in the UI) may // not be visible private volatile boolean memoryBarrier; private final CompletionCallback completionCallback; // ideally would use AtomicInteger here, but using plain volatile int as optimization since // it's ok if race condition in limit check private volatile int entryLimitCounter; private volatile int extraErrorEntryLimitCounter; private volatile int aggregateQueryLimitCounter; private volatile int aggregateServiceCallLimitCounter; private volatile @Nullable AtomicInteger throwableFrameLimitCounter; private final ThreadContextImpl mainThreadContext; @GuardedBy("mainThreadContext") private @MonotonicNonNull List<ThreadContextImpl> auxThreadContexts; @GuardedBy("mainThreadContext") private @MonotonicNonNull Set<ThreadContextImpl> unmergedLimitExceededAuxThreadContexts; // async root timers are the root timers which do not have corresponding thread context // (those corresponding to async trace entries) private final Object asyncTimerLock = new Object(); @GuardedBy("asyncTimerLock") private @MonotonicNonNull List<AsyncTimerImpl> asyncTimers; @GuardedBy("asyncTimerLock") private @MonotonicNonNull Map<String, AggregateAsyncTimer> aggregateAsyncTimers; private volatile boolean completed; private volatile long endTick; private final Ticker ticker; private final UserProfileScheduler userProfileScheduler; private @Nullable TransactionEntry transactionEntry; @GuardedBy("mainThreadContext") private @MonotonicNonNull RootTimerCollectorImpl alreadyMergedAuxThreadTimers; @GuardedBy("mainThreadContext") private @MonotonicNonNull ThreadStatsCollectorImpl alreadyMergedAuxThreadStats; @GuardedBy("mainThreadContext") private boolean stopMergingAuxThreadContexts; Transaction(long startTime, long startTick, String transactionType, String transactionName, MessageSupplier messageSupplier, TimerName timerName, boolean captureThreadStats, int maxTraceEntriesPerTransaction, int maxAggregateQueriesPerType, int maxAggregateServiceCallsPerType, @Nullable ThreadAllocatedBytes threadAllocatedBytes, CompletionCallback completionCallback, Ticker ticker, TransactionRegistry transactionRegistry, TransactionServiceImpl transactionService, ConfigService configService, UserProfileScheduler userProfileScheduler, Holder</*@Nullable*/ ThreadContextImpl> threadContextHolder) { this.startTime = startTime; this.startTick = startTick; this.transactionType = transactionType; this.transactionName = transactionName; this.maxTraceEntriesPerTransaction = maxTraceEntriesPerTransaction; this.maxAggregateQueriesPerType = maxAggregateQueriesPerType; this.maxAggregateServiceCallsPerType = maxAggregateServiceCallsPerType; this.completionCallback = completionCallback; this.ticker = ticker; this.userProfileScheduler = userProfileScheduler; this.transactionRegistry = transactionRegistry; this.transactionService = transactionService; this.configService = configService; mainThreadContext = new ThreadContextImpl(castInitialized(this), null, null, messageSupplier, timerName, startTick, captureThreadStats, threadAllocatedBytes, false, ticker, threadContextHolder, null); } long getStartTime() { return startTime; } public String getTraceId() { if (traceId == null) { // double-checked locking works here because traceId is volatile // // synchronized on "this" as a micro-optimization just so don't need to create an empty // object to lock on synchronized (this) { if (traceId == null) { traceId = buildTraceId(startTime); } } } return traceId; } public long getStartTick() { return startTick; } public boolean isCompleted() { return completed; } long getEndTick() { return endTick; } public long getDurationNanos() { return completed ? endTick - startTick : ticker.read() - startTick; } public String getTransactionType() { return transactionType; } public String getTransactionName() { return transactionName; } public String getHeadline() { Object messageSupplier = mainThreadContext.getRootEntry().getMessageSupplier(); // root trace entry messageSupplier is never null checkNotNull(messageSupplier); // root trace entry messageSupplier is never QueryMessageSupplier return ((ReadableMessage) ((MessageSupplier) messageSupplier).get()).getText(); } public String getUser() { return Strings.nullToEmpty(user); } public ImmutableSetMultimap<String, String> getAttributes() { if (attributes == null) { return ImmutableSetMultimap.of(); } SetMultimap<String, String> orderedAttributes = TreeMultimap.create(); synchronized (attributes) { orderedAttributes.putAll(attributes); } return ImmutableSetMultimap.copyOf(orderedAttributes); } Map<String, ?> getDetail() { Object messageSupplier = mainThreadContext.getRootEntry().getMessageSupplier(); // root trace entry messageSupplier is never null checkNotNull(messageSupplier); // root trace entry messageSupplier is never QueryMessageSupplier return ((ReadableMessage) ((MessageSupplier) messageSupplier).get()).getDetail(); } public @Nullable ErrorMessage getErrorMessage() { // don't prefer the root entry error message since it is likely a more generic error // message, e.g. servlet response sendError(500) if (errorMessage != null) { return errorMessage; } return mainThreadContext.getRootEntry().getErrorMessage(); } boolean isAsync() { return async; } boolean isOuter() { return outer; } TimerImpl getMainThreadRootTimer() { memoryBarrierRead(); return mainThreadContext.getRootTimer(); } void mergeAuxThreadTimersInto(RootTimerCollector rootTimers) { synchronized (mainThreadContext) { if (auxThreadContexts == null) { return; } for (ThreadContextImpl auxThreadContext : auxThreadContexts) { rootTimers.mergeRootTimer(auxThreadContext.getRootTimer()); } if (alreadyMergedAuxThreadTimers != null) { for (CommonTimerImpl rootTimer : alreadyMergedAuxThreadTimers.getRootTimers()) { rootTimers.mergeRootTimer(rootTimer); } } if (unmergedLimitExceededAuxThreadContexts != null) { for (ThreadContextImpl auxThreadContext : unmergedLimitExceededAuxThreadContexts) { rootTimers.mergeRootTimer(auxThreadContext.getRootTimer()); } } } } void mergeAsyncTimersInto(RootTimerCollector rootTimers) { memoryBarrierRead(); synchronized (asyncTimerLock) { if (asyncTimers == null) { return; } for (AsyncTimerImpl asyncTimer : asyncTimers) { rootTimers.mergeRootTimer(asyncTimer); } if (aggregateAsyncTimers == null) { return; } for (Entry<String, AggregateAsyncTimer> entry : aggregateAsyncTimers.entrySet()) { AggregateAsyncTimer value = entry.getValue(); rootTimers.mergeRootTimer( new SimpleTimerImpl(entry.getKey(), value.totalNanos, value.count)); } } } // can be called from a non-transaction thread ThreadStats getMainThreadStats() { return mainThreadContext.getThreadStats(); } public long getTotalCpuNanos() { long totalCpuNanos = mainThreadContext.getTotalCpuNanos(); synchronized (mainThreadContext) { if (auxThreadContexts == null) { return totalCpuNanos; } for (ThreadContextImpl auxThreadContext : auxThreadContexts) { totalCpuNanos = NotAvailableAware.add(totalCpuNanos, auxThreadContext.getTotalCpuNanos()); } return totalCpuNanos; } } void mergeAuxThreadStatsInto(ThreadStatsCollector threadStats) { synchronized (mainThreadContext) { if (auxThreadContexts == null) { return; } for (ThreadContextImpl auxThreadContext : auxThreadContexts) { threadStats.mergeThreadStats(auxThreadContext.getThreadStats()); } if (alreadyMergedAuxThreadStats != null) { threadStats.mergeThreadStats(alreadyMergedAuxThreadStats.getMergedThreadStats()); } if (unmergedLimitExceededAuxThreadContexts != null) { for (ThreadContextImpl auxThreadContext : unmergedLimitExceededAuxThreadContexts) { threadStats.mergeThreadStats(auxThreadContext.getThreadStats()); } } } } void mergeQueriesInto(QueryCollector queries) { memoryBarrierRead(); mainThreadContext.mergeQueriesInto(queries); synchronized (mainThreadContext) { if (auxThreadContexts != null) { for (ThreadContextImpl auxThreadContext : auxThreadContexts) { auxThreadContext.mergeQueriesInto(queries); } } } } void mergeServiceCallsInto(ServiceCallCollector serviceCalls) { memoryBarrierRead(); mainThreadContext.mergeServiceCallsInto(serviceCalls); synchronized (mainThreadContext) { if (auxThreadContexts != null) { for (ThreadContextImpl auxThreadContext : auxThreadContexts) { auxThreadContext.mergeServiceCallsInto(serviceCalls); } } } } // this method has side effect of incrementing counter boolean allowAnotherEntry() { return entryLimitCounter++ < maxTraceEntriesPerTransaction; } // this method has side effect of incrementing counter boolean allowAnotherErrorEntry() { // use higher entry limit when adding errors, but still need some kind of cap return entryLimitCounter++ < maxTraceEntriesPerTransaction || extraErrorEntryLimitCounter++ < maxTraceEntriesPerTransaction; } // this method has side effect of incrementing counter boolean allowAnotherAggregateQuery(boolean bypassLimit) { if (aggregateQueryLimitCounter++ < maxAggregateQueriesPerType * AdvancedConfig.OVERALL_AGGREGATE_QUERIES_HARD_LIMIT_MULTIPLIER) { return true; } return bypassLimit; } // this method has side effect of incrementing counter boolean allowAnotherAggregateServiceCall() { return aggregateServiceCallLimitCounter++ < maxAggregateServiceCallsPerType * AdvancedConfig.OVERALL_AGGREGATE_SERVICE_CALLS_HARD_LIMIT_MULTIPLIER; } public void accept(long captureTick, EntryVisitor entryVisitor) throws Exception { memoryBarrierRead(); ListMultimap<TraceEntryImpl, ThreadContextImpl> priorEntryChildThreadContextMap = buildPriorEntryChildThreadContextMap(); ListMultimap<TraceEntryImpl, TraceEntryImpl> parentChildMap = ArrayListMultimap.create(); mainThreadContext.populateParentChildMap(parentChildMap, captureTick, priorEntryChildThreadContextMap); synchronized (mainThreadContext) { if (auxThreadContexts != null) { for (ThreadContextImpl auxThreadContext : auxThreadContexts) { auxThreadContext.populateParentChildMap(parentChildMap, captureTick, priorEntryChildThreadContextMap); } } } new ParentChildMapTrimmer(mainThreadContext.getRootEntry(), parentChildMap, captureTick) .traverse(); addProtobufChildEntries(mainThreadContext.getRootEntry(), parentChildMap, startTick, captureTick, 0, entryVisitor, async); } long getMainThreadProfileSampleCount() { if (mainThreadProfile == null) { return 0; } else { return mainThreadProfile.getSampleCount(); } } @Nullable Profile getMainThreadProfile() { return mainThreadProfile; } public @Nullable org.glowroot.wire.api.model.ProfileOuterClass.Profile getMainThreadProfileProtobuf() { if (mainThreadProfile == null) { return null; } return mainThreadProfile.toProto(); } boolean isMainThreadProfileSampleLimitExceeded() { // TODO implement profile limit return false; } long getAuxThreadProfileSampleCount() { if (auxThreadProfile == null) { return 0; } else { return auxThreadProfile.getSampleCount(); } } @Nullable Profile getAuxThreadProfile() { return auxThreadProfile; } public @Nullable org.glowroot.wire.api.model.ProfileOuterClass.Profile getAuxThreadProfileProtobuf() { if (auxThreadProfile == null) { return null; } return auxThreadProfile.toProto(); } boolean isAuxThreadProfileSampleLimitExceeded() { // TODO implement profile limit return false; } int getSlowThresholdMillisOverride() { return slowThresholdMillis; } public @Nullable Cancellable getImmedateTraceStoreRunnable() { return immedateTraceStoreRunnable; } public boolean isPartiallyStored() { return partiallyStored; } public ThreadContextImpl getMainThreadContext() { return mainThreadContext; } public List<ThreadContextImpl> getActiveAuxThreadContexts() { synchronized (mainThreadContext) { if (auxThreadContexts == null) { return ImmutableList.of(); } List<ThreadContextImpl> activeAuxThreadContexts = Lists.newArrayList(); for (ThreadContextImpl auxThreadContext : auxThreadContexts) { if (auxThreadContext.isActive()) { activeAuxThreadContexts.add(auxThreadContext); } } return activeAuxThreadContexts; } } void setAsync() { async = true; } void setOuter() { outer = true; } void setTransactionType(String transactionType, int priority) { if (priority > transactionTypePriority && !transactionType.isEmpty()) { this.transactionType = transactionType; transactionTypePriority = priority; } } void setTransactionName(String transactionName, int priority) { if (priority > transactionNamePriority && !transactionName.isEmpty()) { this.transactionName = transactionName; transactionNamePriority = priority; } } void setUser(String user, int priority) { if (priority > userPriority && !user.isEmpty()) { this.user = user; userPriority = priority; if (userProfileRunnable == null) { userProfileScheduler.maybeScheduleUserProfiling(this, user); } } } void addAttribute(String name, @Nullable String value) { if (attributes == null) { // no race condition here since only transaction thread calls addAttribute() attributes = HashMultimap.create(ATTRIBUTE_KEYS_INITIAL_CAPACITY, 1); } String val = Strings.nullToEmpty(value); synchronized (attributes) { Collection<String> values = attributes.get(name); if (values.size() < ATTRIBUTE_VALUES_PER_KEY_LIMIT) { values.add(val); } } } void setError(@Nullable String message, @Nullable Throwable t) { if (this.errorMessage == null) { this.errorMessage = ErrorMessage.create(message, t, getThrowableFrameLimitCounter()); } } void setSlowThresholdMillis(int slowThresholdMillis, int priority) { if (priority > slowThresholdMillisPriority) { this.slowThresholdMillis = slowThresholdMillis; slowThresholdMillisPriority = priority; } else if (priority == slowThresholdMillisPriority) { // use the minimum threshold from the same override source this.slowThresholdMillis = Math.min(this.slowThresholdMillis, slowThresholdMillis); } } void setUserProfileRunnable(Cancellable userProfileRunnable) { if (this.userProfileRunnable != null) { logger.warn("setUserProfileRunnable(): overwriting non-null userProfileRunnable"); } this.userProfileRunnable = userProfileRunnable; } public void setImmediateTraceStoreRunnable(Cancellable immedateTraceStoreRunnable) { if (this.immedateTraceStoreRunnable != null) { logger.warn("setImmediateTraceStoreRunnable(): overwriting non-null" + " immedateTraceStoreRunnable"); } this.immedateTraceStoreRunnable = immedateTraceStoreRunnable; } void setPartiallyStored() { partiallyStored = true; } void setTransactionEntry(TransactionEntry transactionEntry) { this.transactionEntry = transactionEntry; } void removeFromActiveTransactions() { checkNotNull(transactionEntry).remove(); } @Nullable ThreadContextImpl startAuxThreadContext(@Nullable TraceEntryImpl parentTraceEntry, @Nullable TraceEntryImpl parentThreadContextPriorEntry, TimerName auxTimerName, long startTick, Holder</*@Nullable*/ ThreadContextImpl> threadContextHolder, @Nullable MessageSupplier servletMessageSupplier, @Nullable ThreadAllocatedBytes threadAllocatedBytes) { ThreadContextImpl auxThreadContext; synchronized (mainThreadContext) { // check completed and add aux thread context inside synchronized block to avoid race // condition with setting completed and detaching incomplete aux thread contexts, see // synchronized block in end() if (completed) { return null; } if (auxThreadContexts == null) { auxThreadContexts = Lists.newArrayList(); } // conditions below for parentTraceEntry and parentThreadContextPriorEntry are redundant // since they will not be null until after allowAnotherAuxThreadContextWithHierarchy() // starts returning false if (allowAnotherAuxThreadContextWithHierarchy() && parentTraceEntry != null && parentThreadContextPriorEntry != null) { auxThreadContext = new ThreadContextImpl(this, parentTraceEntry, parentThreadContextPriorEntry, AuxThreadRootMessageSupplier.INSTANCE, auxTimerName, startTick, mainThreadContext.getCaptureThreadStats(), threadAllocatedBytes, false, ticker, threadContextHolder, servletMessageSupplier); auxThreadContexts.add(auxThreadContext); } else { auxThreadContext = new ThreadContextImpl(this, mainThreadContext.getRootEntry(), mainThreadContext.getTailEntry(), AuxThreadRootMessageSupplier.INSTANCE, auxTimerName, startTick, mainThreadContext.getCaptureThreadStats(), threadAllocatedBytes, true, ticker, threadContextHolder, servletMessageSupplier); if (unmergedLimitExceededAuxThreadContexts == null) { unmergedLimitExceededAuxThreadContexts = Sets.newHashSet(); } unmergedLimitExceededAuxThreadContexts.add(auxThreadContext); } } // see counterpart to this synchronization (and explanation) in ThreadContextImpl.detach() synchronized (threadContextHolder) { threadContextHolder.set(auxThreadContext); } return auxThreadContext; } void mergeLimitExceededAuxThreadContext(ThreadContextImpl auxThreadContext) { synchronized (mainThreadContext) { checkNotNull(unmergedLimitExceededAuxThreadContexts).remove(auxThreadContext); if (auxThreadContext.hasTraceEntries()) { checkNotNull(auxThreadContexts).add(auxThreadContext); return; } if (alreadyMergedAuxThreadTimers == null) { alreadyMergedAuxThreadTimers = new RootTimerCollectorImpl(); } if (alreadyMergedAuxThreadStats == null) { alreadyMergedAuxThreadStats = new ThreadStatsCollectorImpl(); } alreadyMergedAuxThreadTimers.mergeRootTimer(auxThreadContext.getRootTimer()); alreadyMergedAuxThreadStats.mergeThreadStats(auxThreadContext.getThreadStats()); } } AsyncTimerImpl startAsyncTimer(TimerName asyncTimerName, long startTick) { AsyncTimerImpl asyncTimer = new AsyncTimerImpl((TimerNameImpl) asyncTimerName, startTick); synchronized (asyncTimerLock) { if (asyncTimers == null) { asyncTimers = Lists.newArrayList(); } if (asyncTimers.size() >= 1000) { // this is just to conserve memory if (aggregateAsyncTimers == null) { aggregateAsyncTimers = Maps.newHashMap(); } List<AsyncTimerImpl> activeAsyncTimers = Lists.newArrayList(); for (AsyncTimerImpl loopAsyncTimer : asyncTimers) { if (loopAsyncTimer.active()) { activeAsyncTimers.add(loopAsyncTimer); continue; } AggregateAsyncTimer aggregateAsyncTimer = aggregateAsyncTimers.get(loopAsyncTimer.getName()); if (aggregateAsyncTimer == null) { aggregateAsyncTimer = new AggregateAsyncTimer(); aggregateAsyncTimers.put(loopAsyncTimer.getName(), aggregateAsyncTimer); } aggregateAsyncTimer.totalNanos += loopAsyncTimer.getTotalNanos(); aggregateAsyncTimer.count += loopAsyncTimer.getCount(); } asyncTimers = activeAsyncTimers; } asyncTimers.add(asyncTimer); } return asyncTimer; } TraceEntryImpl startInnerTransaction(String transactionType, String transactionName, MessageSupplier messageSupplier, TimerName timerName, Holder</*@Nullable*/ ThreadContextImpl> threadContextHolder) { return transactionService.startTransaction(transactionType, transactionName, messageSupplier, timerName, threadContextHolder); } boolean isEntryLimitExceeded() { return entryLimitCounter > maxTraceEntriesPerTransaction; } void captureStackTrace(boolean auxiliary, ThreadInfo threadInfo, int limit) { if (completed) { return; } Profile profile; if (auxiliary) { profile = auxThreadProfile; } else { profile = mainThreadProfile; } if (profile == null) { // initialization possible race condition (between StackTraceCollector and // UserProfileRunnable) is ok, worst case scenario it misses an almost simultaneously // captured stack trace // // profile is constructed and first stack trace is added prior to setting the // transaction profile field, so that it is not possible to read a profile that doesn't // have at least one stack trace profile = new Profile(); profile.addStackTrace(threadInfo, limit); if (auxiliary) { auxThreadProfile = profile; } else { mainThreadProfile = profile; } return; } profile.addStackTrace(threadInfo, limit); } void end(long endTick, boolean completeAsyncTransaction) { if (async && !completeAsyncTransaction) { return; } // set endTick first before completed, to avoid race condition in getDurationNanos() this.endTick = endTick; synchronized (mainThreadContext) { // set completed and detach incomplete aux thread contexts inside synchronized block // to avoid race condition with adding new aux thread contexts, see synchronized block // in startAuxThreadContext() completed = true; detachIncompleteAuxThreadContexts(); } if (immedateTraceStoreRunnable != null) { immedateTraceStoreRunnable.cancel(); } if (userProfileRunnable != null) { userProfileRunnable.cancel(); } completionCallback.completed(this); } // called by the transaction thread void onCompleteWillStoreTrace(long captureTime) { this.captureTime = captureTime; } long getCaptureTime() { return captureTime; } TransactionRegistry getTransactionRegistry() { return transactionRegistry; } TransactionServiceImpl getTransactionService() { return transactionService; } ConfigService getConfigService() { return configService; } AtomicInteger getThrowableFrameLimitCounter() { if (throwableFrameLimitCounter == null) { // double-checked locking works here because throwableFrameLimitCounter is volatile // // synchronized on "this" as a micro-optimization just so don't need to create an empty // object to lock on synchronized (this) { if (throwableFrameLimitCounter == null) { throwableFrameLimitCounter = new AtomicInteger(); } } } return throwableFrameLimitCounter; } boolean memoryBarrierRead() { return memoryBarrier; } void memoryBarrierWrite() { memoryBarrier = true; } void memoryBarrierReadWrite() { memoryBarrierRead(); memoryBarrierWrite(); } // must be called under synchronized (mainThreadContext) @RequiresNonNull("auxThreadContexts") private boolean allowAnotherAuxThreadContextWithHierarchy() { if (auxThreadContexts.size() < TRANSACTION_AUX_THREAD_CONTEXT_LIMIT) { return true; } if (stopMergingAuxThreadContexts) { return false; } List<ThreadContextImpl> mergeableAuxThreadContexts = Lists.newArrayList(); List<ThreadContextImpl> nonMergeableAuxThreadContexts = Lists.newArrayList(); for (Iterator<ThreadContextImpl> i = auxThreadContexts.iterator(); i.hasNext();) { ThreadContextImpl loopAuxThreadContext = i.next(); if (loopAuxThreadContext.isCompleteAndEmptyExceptForTimersAndThreadStats()) { mergeableAuxThreadContexts.add(loopAuxThreadContext); } else { nonMergeableAuxThreadContexts.add(loopAuxThreadContext); } } if (mergeableAuxThreadContexts.size() < 0.1 * auxThreadContexts.size()) { // unable to merge more than 10% stopMergingAuxThreadContexts = true; return false; } if (alreadyMergedAuxThreadTimers == null) { alreadyMergedAuxThreadTimers = new RootTimerCollectorImpl(); } if (alreadyMergedAuxThreadStats == null) { alreadyMergedAuxThreadStats = new ThreadStatsCollectorImpl(); } for (ThreadContextImpl mergeableAuxThreadContext : mergeableAuxThreadContexts) { alreadyMergedAuxThreadTimers.mergeRootTimer(mergeableAuxThreadContext.getRootTimer()); alreadyMergedAuxThreadStats .mergeThreadStats(mergeableAuxThreadContext.getThreadStats()); } auxThreadContexts = nonMergeableAuxThreadContexts; return true; } private static <T extends Exception> void addProtobufChildEntries(TraceEntryImpl entry, ListMultimap<TraceEntryImpl, TraceEntryImpl> parentChildMap, long transactionStartTick, long captureTick, int depth, EntryVisitor entryVisitor, boolean removeSingleAuxEntry) throws Exception { if (!parentChildMap.containsKey(entry)) { // check containsKey to avoid creating garbage empty list via ListMultimap return; } Collection<TraceEntryImpl> childEntries = parentChildMap.get(entry); for (TraceEntryImpl childEntry : childEntries) { boolean singleAuxEntry = childEntries.size() == 1 && childEntry.isAuxThreadRoot(); if (singleAuxEntry && removeSingleAuxEntry) { addProtobufChildEntries(childEntry, parentChildMap, transactionStartTick, captureTick, depth, entryVisitor, removeSingleAuxEntry); } else { childEntry.accept(depth, transactionStartTick, captureTick, entryVisitor); addProtobufChildEntries(childEntry, parentChildMap, transactionStartTick, captureTick, depth + 1, entryVisitor, false); } } } private ListMultimap<TraceEntryImpl, ThreadContextImpl> buildPriorEntryChildThreadContextMap() { synchronized (mainThreadContext) { if (auxThreadContexts == null) { return ImmutableListMultimap.of(); } ListMultimap<TraceEntryImpl, ThreadContextImpl> parentChildMap = ArrayListMultimap.create(); for (ThreadContextImpl auxThreadContext : auxThreadContexts) { // checkNotNull is safe b/c aux thread contexts have non-null parent thread context // prior entries when they are not limit exceeded aux thread contexts parentChildMap.put( checkNotNull(auxThreadContext.getParentThreadContextPriorEntry()), auxThreadContext); } return parentChildMap; } } // must be called under synchronized (mainThreadContext) private void detachIncompleteAuxThreadContexts() { if (auxThreadContexts == null) { return; } for (ThreadContextImpl auxThreadContext : auxThreadContexts) { if (auxThreadContext.isCompleted()) { continue; } auxThreadContext.detach(); if (!logger.isDebugEnabled()) { continue; } ThreadInfo threadInfo = ManagementFactory.getThreadMXBean() .getThreadInfo(auxThreadContext.getThreadId(), Integer.MAX_VALUE); if (logger.isDebugEnabled() && !isCompleted() && threadInfo != null) { // still not complete and got a valid stack trace from auxiliary thread StringBuilder sb = new StringBuilder(); for (StackTraceElement stackTraceElement : threadInfo.getStackTrace()) { sb.append(" "); sb.append(stackTraceElement.toString()); sb.append('\n'); } logger.debug( "auxiliary thread extended beyond the transaction which started it\n{}", sb); } } } @VisibleForTesting static String buildTraceId(long startTime) { byte[] bytes = new byte[10]; random.nextBytes(bytes); // lower 6 bytes of current time will wrap only every 8925 years return lowerSixBytesHex(startTime) + BaseEncoding.base16().lowerCase().encode(bytes); } @VisibleForTesting static String lowerSixBytesHex(long startTime) { long mask = 1L << 48; return Long.toHexString(mask | (startTime & (mask - 1))).substring(1); } interface CompletionCallback { void completed(Transaction transaction); } interface RootTimerCollector { void mergeRootTimer(CommonTimerImpl rootTimer); } interface ThreadStatsCollector { void mergeThreadStats(ThreadStats threadStats); } private static class AggregateAsyncTimer { private long totalNanos; private long count; } private static class SimpleTimerImpl implements CommonTimerImpl { private final String name; private final long totalNanos; private final long count; private SimpleTimerImpl(String name, long totalNanos, long count) { this.name = name; this.totalNanos = totalNanos; this.count = count; } @Override public String getName() { return name; } @Override public boolean isExtended() { return false; } @Override public long getTotalNanos() { return totalNanos; } @Override public long getCount() { return count; } @Override public void mergeChildTimersInto(List<MutableTraceTimer> childTimers) { // async timers have no child timers } @Override public void mergeChildTimersInto2(List<MutableAggregateTimer> childTimers) { // async timers have no child timers } @Override public TimerImplSnapshot getSnapshot() { return ImmutableTimerImplSnapshot.builder() .totalNanos(totalNanos) .count(count) .active(false) .build(); } } private static class AuxThreadRootMessageSupplier extends MessageSupplier { private static final AuxThreadRootMessageSupplier INSTANCE = new AuxThreadRootMessageSupplier(); private final Message message = Message.create(AUXILIARY_THREAD_MESSAGE); @Override public Message get() { return message; } } private static class ParentChildMapTrimmer extends Traverser<TraceEntryImpl, RuntimeException> { private final ListMultimap<TraceEntryImpl, TraceEntryImpl> parentChildMap; private final long captureTick; private ParentChildMapTrimmer(TraceEntryImpl rootNode, ListMultimap<TraceEntryImpl, TraceEntryImpl> parentChildMap, long captureTick) { super(rootNode); this.parentChildMap = parentChildMap; this.captureTick = captureTick; } @Override public List<TraceEntryImpl> visit(TraceEntryImpl node, int depth) { if (!parentChildMap.containsKey(node)) { // check containsKey to avoid creating garbage empty list via ListMultimap return ImmutableList.of(); } return parentChildMap.get(node); } @Override public void revisitAfterChildren(TraceEntryImpl node) { if (!parentChildMap.containsKey(node)) { // check containsKey to avoid creating garbage empty list via ListMultimap return; } List<TraceEntryImpl> childEntries = parentChildMap.get(node); ListIterator<TraceEntryImpl> i = childEntries.listIterator(); while (i.hasNext()) { TraceEntryImpl childEntry = i.next(); if (childEntry.getStartTick() > captureTick) { i.remove(); } } i = childEntries.listIterator(); while (i.hasNext()) { TraceEntryImpl childEntry = i.next(); if (!childEntry.isAuxThreadRoot()) { continue; } if (!parentChildMap.containsKey(childEntry)) { // check containsKey to avoid creating garbage empty list via ListMultimap // // remove empty aux thread root i.remove(); continue; } List<TraceEntryImpl> subChildEntries = parentChildMap.get(childEntry); if (subChildEntries.isEmpty()) { // remove empty aux thread root i.remove(); continue; } if (subChildEntries.size() == 1) { TraceEntryImpl singleSubChildEntry = subChildEntries.get(0); if (singleSubChildEntry.isAuxThreadRoot()) { // compact consecutive single aux root entries i.set(singleSubChildEntry); } } } } } }