From a574381300648a5f40a86c4c4a797af07a8415dd Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Thu, 23 Nov 2023 12:28:00 +0100 Subject: [PATCH 01/18] Copied and formatted original profiling plugin from apm agent --- .../elastic/apm/otel/profiler/CallTree.java | 861 +++++++++++++ .../apm/otel/profiler/FixedNanoClock.java | 36 + .../elastic/apm/otel/profiler/NanoClock.java | 24 + .../profiler/ProfilingActivationListener.java | 58 + .../otel/profiler/ProfilingConfiguration.java | 266 ++++ .../apm/otel/profiler/ProfilingFactory.java | 55 + .../apm/otel/profiler/SamplingProfiler.java | 1126 +++++++++++++++++ .../apm/otel/profiler/SystemNanoClock.java | 26 + .../apm/otel/profiler/ThreadMatcher.java | 65 + .../profiler/asyncprofiler/AsyncProfiler.java | 202 +++ .../profiler/asyncprofiler/BufferedFile.java | 328 +++++ .../profiler/asyncprofiler/JfrParser.java | 484 +++++++ .../profiler/asyncprofiler/package-info.java | 22 + .../profiler/collections/CollectionUtil.java | 66 + .../otel/profiler/collections/Hashing.java | 131 ++ .../profiler/collections/Int2IntHashMap.java | 863 +++++++++++++ .../collections/Int2ObjectHashMap.java | 790 ++++++++++++ .../profiler/collections/IntIntConsumer.java | 31 + .../collections/Long2LongHashMap.java | 864 +++++++++++++ .../collections/Long2ObjectHashMap.java | 789 ++++++++++++ .../profiler/collections/LongHashSet.java | 689 ++++++++++ .../collections/LongLongConsumer.java | 31 + .../profiler/collections/package-info.java | 28 + .../apm/otel/profiler/package-info.java | 22 + .../libasyncProfiler-linux-aarch64.so | Bin 0 -> 262320 bytes .../libasyncProfiler-linux-arm.so | Bin 0 -> 232852 bytes .../libasyncProfiler-linux-x64.so | Bin 0 -> 262937 bytes .../libasyncProfiler-linux-x86.so | Bin 0 -> 243568 bytes .../libasyncProfiler-macos-x64.so | Bin 0 -> 216640 bytes .../otel/profiler/CallTreeSpanifyTest.java | 165 +++ .../apm/otel/profiler/CallTreeTest.java | 1043 +++++++++++++++ .../profiler/SamplingProfilerQueueTest.java | 65 + .../otel/profiler/SamplingProfilerReplay.java | 76 ++ .../otel/profiler/SamplingProfilerTest.java | 324 +++++ .../apm/otel/profiler/ThreadMatcherTest.java | 51 + .../asyncprofiler/AsyncProfilerTest.java | 66 + .../asyncprofiler/AsyncProfilerUpgrader.java | 224 ++++ .../profiler/asyncprofiler/JfrParserTest.java | 63 + .../src/test/resources/recording.jfr | Bin 0 -> 59679 bytes 39 files changed, 9934 insertions(+) create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/FixedNanoClock.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingConfiguration.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingFactory.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SystemNanoClock.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/package-info.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/package-info.java create mode 100755 inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so create mode 100755 inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so create mode 100755 inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so create mode 100755 inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so create mode 100755 inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java create mode 100644 inferred-spans/src/test/resources/recording.jfr diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java new file mode 100644 index 000000000..0133e09ac --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java @@ -0,0 +1,861 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.StackFrame; +import co.elastic.apm.agent.impl.transaction.TraceContext; +import co.elastic.apm.agent.profiler.collections.LongHashSet; +import co.elastic.apm.agent.sdk.internal.collections.LongList; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import co.elastic.apm.agent.tracer.pooling.ObjectPool; +import co.elastic.apm.agent.tracer.pooling.Recyclable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Converts a sequence of stack traces into a tree structure of method calls. + * + *
+ *             count
+ *  b b     a      4
+ * aaaa ──► ├─b    1
+ *          └─b    1
+ * 
+ * + *

It also stores information about which span is the parent of a particular call tree node, + * based on which span has been {@linkplain ElasticApmTracer#getActive() active} at that time. + * + *

This allows to {@linkplain Root#spanify() infer spans from the call tree} which have the + * correct parent/child relationships with the regular spans. + */ +public class CallTree implements Recyclable { + + private static final int INITIAL_CHILD_SIZE = 2; + @Nullable private CallTree parent; + protected int count; + private List children = new ArrayList<>(INITIAL_CHILD_SIZE); + @Nullable private StackFrame frame; + protected long start; + private long lastSeen; + private boolean ended; + private long activationTimestamp = -1; + + /** + * The context of the transaction or span which is the direct parent of this call tree node. Used + * in {@link #spanify} to override the parent. + */ + @Nullable private TraceContext activeContextOfDirectParent; + + private long deactivationTimestamp = -1; + private boolean isSpan; + private int depth; + + /** + * @see co.elastic.apm.agent.impl.transaction.AbstractSpan#childIds + */ + @Nullable private LongList childIds; + + @Nullable private LongList maybeChildIds; + + public CallTree() {} + + public void set(@Nullable CallTree parent, StackFrame frame, long nanoTime) { + this.parent = parent; + this.frame = frame; + this.start = nanoTime; + if (parent != null) { + this.depth = parent.depth + 1; + } + } + + public boolean isSuccessor(CallTree parent) { + if (depth > parent.depth) { + return getNthParent(depth - parent.depth) == parent; + } + return false; + } + + @Nullable + public CallTree getNthParent(int n) { + CallTree parent = this; + for (int i = 0; i < n; i++) { + if (parent != null) { + parent = parent.parent; + } else { + return null; + } + } + return parent; + } + + public void activation(TraceContext traceContext, long activationTimestamp) { + this.activeContextOfDirectParent = traceContext; + this.activationTimestamp = activationTimestamp; + } + + protected void handleDeactivation( + TraceContext deactivatedSpan, long activationTimestamp, long deactivationTimestamp) { + if (deactivatedSpan.idEquals(activeContextOfDirectParent)) { + this.deactivationTimestamp = deactivationTimestamp; + } else { + CallTree lastChild = getLastChild(); + if (lastChild != null) { + lastChild.handleDeactivation(deactivatedSpan, activationTimestamp, deactivationTimestamp); + } + } + // if an actual child span is deactivated after this call tree node has ended + // it means that this node has actually ended at least at the same point, if not after, the + // actual span has been deactivated + // + // [a(inferred)] ─► [a(inferred) ] ← set end timestamp to timestamp of deactivation of b + // └─[b(actual) ] └─[b(actual) ] + // see also CallTreeTest::testDectivationAfterEnd + if (happenedDuring(activationTimestamp) && happenedAfter(deactivationTimestamp)) { + lastSeen = deactivationTimestamp; + } + } + + private boolean happenedDuring(long timestamp) { + return start <= timestamp && timestamp <= lastSeen; + } + + private boolean happenedAfter(long timestamp) { + return lastSeen < timestamp; + } + + public static CallTree.Root createRoot( + ObjectPool rootPool, + byte[] traceContext, + @Nullable String serviceName, + @Nullable String serviceVersion, + long nanoTime) { + CallTree.Root root = rootPool.createInstance(); + root.set(traceContext, serviceName, serviceVersion, nanoTime); + return root; + } + + /** + * Adds a single stack trace to the call tree which either updates the {@link #lastSeen} timestamp + * of an existing call tree node, {@linkplain #end ends} a node, or {@linkplain #addChild adds a + * new child}. + * + * @param stackFrames the stack trace which is iterated over in reverse order + * @param index the current index of {@code stackFrames} + * @param activeSpan the trace context of the currently {@linkplain ElasticApmTracer#getActive()} + * active transaction/span + * @param activationTimestamp the timestamp of when {@code traceContext} has been activated + * @param nanoTime the timestamp of when this stack trace has been recorded + * @param callTreePool + * @param minDurationNs + * @param root + */ + protected CallTree addFrame( + List stackFrames, + int index, + @Nullable TraceContext activeSpan, + long activationTimestamp, + long nanoTime, + ObjectPool callTreePool, + long minDurationNs, + Root root) { + count++; + lastSeen = nanoTime; + // c ee ← traceContext not set - they are not a child of the active span but the frame + // below them + // bbb dd ← traceContext set + // ------ ← all new CallTree during this period should have the traceContext set + // a aaaaaa a + // | | + // active deactive + + // this branch is already aware of the activation + // this means the provided activeSpan is not a direct parent of new child nodes + if (activeSpan != null + && this.activeContextOfDirectParent != null + && this.activeContextOfDirectParent.idEquals(activeSpan)) { + activeSpan = null; + } + + // non-last children are already ended by definition + CallTree lastChild = getLastChild(); + // if the frame corresponding to the last child is not in the stack trace + // it's assumed to have ended one tick ago + CallTree topOfStack = this; + boolean endChild = true; + if (index >= 1) { + final StackFrame frame = stackFrames.get(--index); + if (lastChild != null) { + if (!lastChild.isEnded() && frame.equals(lastChild.frame)) { + topOfStack = + lastChild.addFrame( + stackFrames, + index, + activeSpan, + activationTimestamp, + nanoTime, + callTreePool, + minDurationNs, + root); + endChild = false; + } else { + topOfStack = + addChild( + frame, + stackFrames, + index, + activeSpan, + activationTimestamp, + nanoTime, + callTreePool, + minDurationNs, + root); + } + } else { + topOfStack = + addChild( + frame, + stackFrames, + index, + activeSpan, + activationTimestamp, + nanoTime, + callTreePool, + minDurationNs, + root); + } + } + if (lastChild != null && !lastChild.isEnded() && endChild) { + lastChild.end(callTreePool, minDurationNs, root); + } + transferMaybeChildIdsToChildIds(); + return topOfStack; + } + + /** + * This method is called when we know for sure that the maybe child ids are actually belonging to + * this call tree. This is the case after we've seen another frame represented by this call tree. + * + * @see #addMaybeChildId(long) + */ + private void transferMaybeChildIdsToChildIds() { + if (maybeChildIds != null) { + if (childIds == null) { + childIds = maybeChildIds; + maybeChildIds = null; + } else { + childIds.addAll(maybeChildIds); + maybeChildIds.clear(); + } + } + } + + private CallTree addChild( + StackFrame frame, + List stackFrames, + int index, + @Nullable TraceContext traceContext, + long activationTimestamp, + long nanoTime, + ObjectPool callTreePool, + long minDurationNs, + Root root) { + CallTree callTree = callTreePool.createInstance(); + callTree.set(this, frame, nanoTime); + if (traceContext != null) { + callTree.activation(traceContext, activationTimestamp); + } + children.add(callTree); + return callTree.addFrame( + stackFrames, index, null, activationTimestamp, nanoTime, callTreePool, minDurationNs, root); + } + + long getDurationUs() { + return getDurationNs() / 1000; + } + + private long getDurationNs() { + return lastSeen - start; + } + + public int getCount() { + return count; + } + + @Nullable + public StackFrame getFrame() { + return frame; + } + + public List getChildren() { + return children; + } + + protected void end(ObjectPool pool, long minDurationNs, Root root) { + ended = true; + // if the parent span has already been deactivated before this call tree node has ended + // it means that this node is actually the parent of the already deactivated span + // make b parent of a and pre-date the start of b to the activation of a + // [a(inferred) ] [a(inferred) ] + // [1 ] ──┐ [b(inferred) ] + // └[b(inferred)] │ [c(inferred)] + // [c(infer.) ] └► [1 ] + // └─[d(i.)] └──[d(i.)] + // see also CallTreeTest::testDeactivationBeforeEnd + if (deactivationHappenedBeforeEnd()) { + start = Math.min(activationTimestamp, start); + if (parent != null) { + // we know there's always exactly one activation in the parent's childIds + // that needs to be transferred to this call tree node + // in the above example, 1's child id would be first transferred from a to b and then from b + // to c + // this ensures that the UI knows that c is the parent of 1 + parent.giveLastChildIdTo(this); + } + + List callTrees = getChildren(); + for (int i = 0, size = callTrees.size(); i < size; i++) { + CallTree child = callTrees.get(i); + child.activation(activeContextOfDirectParent, activationTimestamp); + child.deactivationTimestamp = deactivationTimestamp; + // re-run this logic for all children, even if they have already ended + child.end(pool, minDurationNs, root); + } + activeContextOfDirectParent = null; + activationTimestamp = -1; + deactivationTimestamp = -1; + } + if (parent != null && isTooFast(minDurationNs)) { + root.previousTopOfStack = parent; + parent.removeChild(pool, this); + } else { + CallTree lastChild = getLastChild(); + if (lastChild != null && !lastChild.isEnded()) { + lastChild.end(pool, minDurationNs, root); + } + } + } + + private boolean isTooFast(long minDurationNs) { + return count == 1 || isFasterThan(minDurationNs); + } + + private void removeChild(ObjectPool pool, CallTree child) { + children.remove(child); + child.recursiveGiveChildIdsTo(this); + child.recycle(pool); + } + + private boolean isFasterThan(long minDurationNs) { + return getDurationNs() < minDurationNs; + } + + private boolean deactivationHappenedBeforeEnd() { + return activeContextOfDirectParent != null + && deactivationTimestamp > -1 + && lastSeen > deactivationTimestamp; + } + + public boolean isLeaf() { + return children.isEmpty(); + } + + /** + * Returns {@code true} if this node has just one child and no self time. + * + *

+   *  c
+   *  b  ← b is a pillar
+   * aaa
+   * 
+ */ + private boolean isPillar() { + return children.size() == 1 && children.get(0).count == count; + } + + @Nullable + public CallTree getLastChild() { + return children.size() > 0 ? children.get(children.size() - 1) : null; + } + + public boolean isEnded() { + return ended; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + try { + toString(sb); + } catch (IOException e) { + throw new RuntimeException(e); + } + return sb.toString(); + } + + private void toString(Appendable out) throws IOException { + toString(out, 0); + } + + private void toString(Appendable out, int level) throws IOException { + for (int i = 0; i < level; i++) { + out.append(" "); + } + out.append(frame != null ? frame.getClassName() : "null") + .append('.') + .append(frame != null ? frame.getMethodName() : "null") + .append(' ') + .append(Integer.toString(count)) + .append('\n'); + for (CallTree node : children) { + node.toString(out, level + 1); + } + } + + int spanify(CallTree.Root root, TraceContext parentContext) { + int createdSpans = 0; + if (activeContextOfDirectParent != null) { + parentContext = activeContextOfDirectParent; + } + Span span = null; + if (!isPillar() || isLeaf()) { + createdSpans++; + span = asSpan(root, parentContext); + this.isSpan = true; + } + List children = getChildren(); + for (int i = 0, size = children.size(); i < size; i++) { + createdSpans += + children.get(i).spanify(root, span != null ? span.getTraceContext() : parentContext); + } + if (span != null) { + span.end(span.getTimestamp() + getDurationUs()); + } + return createdSpans; + } + + protected Span asSpan(Root root, TraceContext parentContext) { + transferMaybeChildIdsToChildIds(); + Span span = + parentContext + .createSpan(root.getEpochMicros(this.start)) + .withType("app") + .withSubtype("inferred"); + + String classFqn = frame.getClassName(); + if (classFqn != null) { + span.appendToName( + classFqn, + co.elastic.apm.agent.tracer.AbstractSpan.PRIORITY_DEFAULT, + frame.getSimpleClassNameOffset(), + classFqn.length()); + } else { + span.appendToName("null"); + } + span.appendToName("#"); + span.appendToName(frame.getMethodName()); + span.withChildIds(childIds); + + // we're not interested in the very bottom of the stack which contains things like accepting and + // handling connections + if (!root.rootContext.idEquals(parentContext)) { + // we're never spanifying the root + assert this.parent != null; + List stackTrace = new ArrayList<>(); + this.parent.fillStackTrace(stackTrace); + span.setStackTrace(stackTrace); + } else { + span.setStackTrace(Collections.emptyList()); + } + return span; + } + + /** Fill in the stack trace up to the parent span */ + private void fillStackTrace(List stackTrace) { + if (parent != null && !this.isSpan) { + stackTrace.add(frame); + parent.fillStackTrace(stackTrace); + } + } + + /** + * Recycles this subtree to the provided pool recursively. Note that this method ends by recycling + * {@code this} node (i.e. - this subtree root), which means that the caller of this method + * should make sure that no reference to this object is held anywhere. + * + *

ALSO NOTE: MAKE SURE NOT TO CALL THIS METHOD FOR {@link CallTree.Root} INSTANCES. + * + * @param pool the pool to which all subtree nodes are to be recycled + */ + public final void recycle(ObjectPool pool) { + assert !(this instanceof Root); + List children = this.children; + for (int i = 0, size = children.size(); i < size; i++) { + children.get(i).recycle(pool); + } + pool.recycle(this); + } + + @Override + public void resetState() { + parent = null; + count = 0; + frame = null; + start = 0; + lastSeen = 0; + ended = false; + activationTimestamp = -1; + activeContextOfDirectParent = null; + deactivationTimestamp = -1; + isSpan = false; + childIds = null; + maybeChildIds = null; + depth = 0; + if (children.size() > INITIAL_CHILD_SIZE) { + // the overwhelming majority of call tree nodes has either one or two children + // don't let outliers grow all lists in the pool over time + children = new ArrayList<>(INITIAL_CHILD_SIZE); + } else { + children.clear(); + } + } + + /** + * When a regular span is activated, we want it's {@link TraceContext#getId() span.id} to be added + * to the call tree that represents the {@linkplain CallTree.Root#topOfStack top of the stack} to + * ensure correct parent/child relationships via re-parenting (See also {@link Span#childIds}). + * + *

However, the {@linkplain CallTree.Root#topOfStack current top of the stack} may turn out to + * not be the right target. Consider this example: + * + *

+   * bb
+   * aa aa
+   *   1  1  ← activation
+   * 
+ * + *

We would add the id of span {@code 1} to {@code b}'s {@link #maybeChildIds}. But after + * seeing the next frame, we realize the {@code b} has already ended and that we should {@link + * #giveMaybeChildIdsTo} from {@code b} and give it to {@code a}. This logic is implemented in + * {@link CallTree.Root#addStackTrace}. After seeing another frame of {@code a}, we know that + * {@code 1} is really the child of {@code a}, so we {@link #transferMaybeChildIdsToChildIds()}. + * + * @param id the child span id to add to this call tree element + */ + public void addMaybeChildId(long id) { + if (maybeChildIds == null) { + maybeChildIds = new LongList(); + } + maybeChildIds.add(id); + } + + public void addChildId(long id) { + if (childIds == null) { + childIds = new LongList(); + } + childIds.add(id); + } + + public boolean hasChildIds() { + return (maybeChildIds != null && maybeChildIds.getSize() > 0) + || (childIds != null && childIds.getSize() > 0); + } + + public void recursiveGiveChildIdsTo(CallTree giveTo) { + for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) { + children.get(i).recursiveGiveChildIdsTo(giveTo); + } + giveChildIdsTo(giveTo); + giveMaybeChildIdsTo(giveTo); + } + + void giveChildIdsTo(CallTree giveTo) { + if (this.childIds == null) { + return; + } + if (giveTo.childIds == null) { + giveTo.childIds = this.childIds; + } else { + giveTo.childIds.addAll(this.childIds); + } + this.childIds = null; + } + + void giveLastChildIdTo(CallTree giveTo) { + if (childIds != null && !childIds.isEmpty()) { + giveTo.addChildId(childIds.remove(childIds.getSize() - 1)); + } + } + + void giveMaybeChildIdsTo(CallTree giveTo) { + if (this.maybeChildIds == null) { + return; + } + if (giveTo.maybeChildIds == null) { + giveTo.maybeChildIds = this.maybeChildIds; + } else { + giveTo.maybeChildIds.addAll(this.maybeChildIds); + } + this.maybeChildIds = null; + } + + public int getDepth() { + return depth; + } + + /** + * A special kind of a {@link CallTree} node which represents the root of the call tree. This acts + * as the interface to the outside to add new nodes to the tree or to update existing ones by + * {@linkplain #addStackTrace adding stack traces}. + */ + public static class Root extends CallTree implements Recyclable { + private static final Logger logger = LoggerFactory.getLogger(Root.class); + private static final StackFrame ROOT_FRAME = new StackFrame("root", "root"); + + /** + * The context of the thread root, mostly a transaction or a span which got activated in an + * auxiliary thread + */ + protected TraceContext rootContext; + + /** + * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() + * active}. This is lazily deserialized from {@link #activeSpanSerialized} if there's an actual + * {@linkplain #addStackTrace stack trace} for this activation. + */ + @Nullable private TraceContext activeSpan; + + /** The timestamp of when {@link #activeSpan} got activated */ + private long activationTimestamp = -1; + + /** + * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() + * active}, in its {@linkplain TraceContext#serialize serialized} form. + */ + private byte[] activeSpanSerialized = new byte[TraceContext.SERIALIZED_LENGTH]; + + @Nullable private CallTree previousTopOfStack; + @Nullable private CallTree topOfStack; + + private final LongHashSet activeSet = new LongHashSet(); + + public Root(ElasticApmTracer tracer) { + this.rootContext = TraceContext.with64BitId(tracer); + } + + private void set( + byte[] traceContext, + @Nullable String serviceName, + @Nullable String serviceVersion, + long nanoTime) { + super.set(null, ROOT_FRAME, nanoTime); + this.rootContext.deserialize(traceContext, serviceName, serviceVersion); + setActiveSpan(traceContext, nanoTime); + } + + public void setActiveSpan(byte[] activeSpanSerialized, long timestamp) { + activationTimestamp = timestamp; + System.arraycopy( + activeSpanSerialized, 0, this.activeSpanSerialized, 0, activeSpanSerialized.length); + this.activeSpan = null; + } + + public void onActivation(byte[] active, long timestamp) { + setActiveSpan(active, timestamp); + if (topOfStack != null) { + long spanId = TraceContext.getSpanId(active); + activeSet.add(spanId); + if (!isNestedActivation(topOfStack)) { + topOfStack.addMaybeChildId(spanId); + } + } + } + + private boolean isNestedActivation(CallTree topOfStack) { + return isAnyActive(topOfStack.childIds) || isAnyActive(topOfStack.maybeChildIds); + } + + private boolean isAnyActive(@Nullable LongList spanIds) { + if (spanIds == null) { + return false; + } + for (int i = 0, size = spanIds.getSize(); i < size; i++) { + if (activeSet.contains(spanIds.get(i))) { + return true; + } + } + return false; + } + + public void onDeactivation(byte[] deactivated, byte[] active, long timestamp) { + if (logger.isDebugEnabled() && !Arrays.equals(activeSpanSerialized, deactivated)) { + logger.warn("Illegal state: deactivating span that is not active"); + } + if (activeSpan != null) { + handleDeactivation(activeSpan, activationTimestamp, timestamp); + } + // else: activeSpan has not been materialized because no stack traces were added during this + // activation + setActiveSpan(active, timestamp); + // we're not interested in tracking nested activations that happen before we see the first + // stack trace + // that's because isNestedActivation is only called if topOfStack != null + // this optimizes for the case where we have no stack traces for a fast executing transaction + if (topOfStack != null) { + long spanId = TraceContext.getSpanId(deactivated); + activeSet.remove(spanId); + } + } + + public void addStackTrace( + ElasticApmTracer tracer, + List stackTrace, + long nanoTime, + ObjectPool callTreePool, + long minDurationNs) { + // only "materialize" trace context if there's actually an associated stack trace to the + // activation + // avoids allocating a TraceContext for very short activations which have no effect on the + // CallTree anyway + boolean firstFrameAfterActivation = false; + if (activeSpan == null) { + firstFrameAfterActivation = true; + activeSpan = TraceContext.with64BitId(tracer); + activeSpan.deserialize( + activeSpanSerialized, rootContext.getServiceName(), rootContext.getServiceVersion()); + } + previousTopOfStack = topOfStack; + topOfStack = + addFrame( + stackTrace, + stackTrace.size(), + activeSpan, + activationTimestamp, + nanoTime, + callTreePool, + minDurationNs, + this); + + // After adding the first frame after an activation, we can check if we added the child ids to + // the correct CallTree + // If the new top of stack is not a successor (a different branch vs just added nodes on the + // same branch) + // we have to transfer the child ids of not yet deactivated spans to the new top of the stack. + // See also CallTreeTest.testActivationAfterMethodEnds and following tests. + if (firstFrameAfterActivation + && previousTopOfStack != topOfStack + && previousTopOfStack != null + && previousTopOfStack.hasChildIds()) { + if (!topOfStack.isSuccessor(previousTopOfStack)) { + CallTree commonAncestor = findCommonAncestor(previousTopOfStack, topOfStack); + CallTree newParent = commonAncestor != null ? commonAncestor : topOfStack; + if (newParent.count > 1) { + previousTopOfStack.giveMaybeChildIdsTo(newParent); + } else if (previousTopOfStack.maybeChildIds != null) { + previousTopOfStack.maybeChildIds.clear(); + } + } + } + } + + @Nullable + private CallTree findCommonAncestor(CallTree previousTopOfStack, CallTree topOfStack) { + int maxDepthOfCommonAncestor = Math.min(previousTopOfStack.getDepth(), topOfStack.getDepth()); + CallTree commonAncestor = null; + // i = 1 avoids considering the CallTree.Root node which is always the same + for (int i = 1; i <= maxDepthOfCommonAncestor; i++) { + CallTree ancestor1 = previousTopOfStack.getNthParent(previousTopOfStack.getDepth() - i); + CallTree ancestor2 = topOfStack.getNthParent(topOfStack.getDepth() - i); + if (ancestor1 == ancestor2) { + commonAncestor = ancestor1; + } else { + break; + } + } + return commonAncestor; + } + + /** + * Creates spans for call tree nodes if they are either not a {@linkplain #isPillar() pillar} or + * are a {@linkplain #isLeaf() leaf}. Nodes which are not converted to {@link Span}s are part of + * the {@link Span#stackFrames} for the nodes which do get converted to a span. + * + *

Parent/child relationships with the regular spans are maintained. One exception is that an + * inferred span can't be the parent of a regular span. That is because the regular spans have + * already been reported once the inferred spans are created. In the future, we might make it + * possible to update the {@link TraceContext#parentId} of a regular span so that it correctly + * reflects being a child of an inferred span. + */ + public int spanify() { + int createdSpans = 0; + List callTrees = getChildren(); + for (int i = 0, size = callTrees.size(); i < size; i++) { + createdSpans += callTrees.get(i).spanify(this, rootContext); + } + return createdSpans; + } + + public TraceContext getRootContext() { + return rootContext; + } + + public long getEpochMicros(long nanoTime) { + return rootContext.getClock().getEpochMicros(nanoTime); + } + + /** + * Recycles this tree to the provided pools. First, all child subtrees are recycled recursively + * to the children pool. Then, {@code this} root node is recycled to the root pool. This means + * that the caller of this method should make sure that no reference to this root object is + * held anywhere. + * + * @param childrenPool object pool for all non-root nodes + * @param rootPool object pool for root nodes + */ + public void recycle(ObjectPool childrenPool, ObjectPool rootPool) { + List children = getChildren(); + for (int i = 0, size = children.size(); i < size; i++) { + children.get(i).recycle(childrenPool); + } + rootPool.recycle(this); + } + + public void end(ObjectPool pool, long minDurationNs) { + end(pool, minDurationNs, this); + } + + @Override + public void resetState() { + super.resetState(); + rootContext.resetState(); + activeSpan = null; + activationTimestamp = -1; + Arrays.fill(activeSpanSerialized, (byte) 0); + previousTopOfStack = null; + topOfStack = null; + activeSet.clear(); + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/FixedNanoClock.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/FixedNanoClock.java new file mode 100644 index 000000000..97fa892b8 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/FixedNanoClock.java @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +public class FixedNanoClock implements NanoClock { + + private long nanoTime = -1L; + + @Override + public long nanoTime() { + if (nanoTime == -1L) { + return System.nanoTime(); + } + return nanoTime; + } + + public void setNanoTime(long nanoTime) { + this.nanoTime = nanoTime; + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java new file mode 100644 index 000000000..02bb3aa8b --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +public interface NanoClock { + + long nanoTime(); +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java new file mode 100644 index 000000000..e7551c52e --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import co.elastic.apm.agent.impl.ActivationListener; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.sdk.internal.ThreadUtil; +import java.util.Objects; + +public class ProfilingActivationListener implements ActivationListener { + + private final ElasticApmTracer tracer; + private final SamplingProfiler profiler; + + public ProfilingActivationListener(ElasticApmTracer tracer) { + this(tracer, Objects.requireNonNull(tracer.getLifecycleListener(SamplingProfiler.class))); + } + + ProfilingActivationListener(ElasticApmTracer tracer, SamplingProfiler profiler) { + this.tracer = tracer; + this.profiler = profiler; + } + + @Override + public void beforeActivate(AbstractSpan context) { + if (context.isSampled() && !ThreadUtil.isVirtual(Thread.currentThread())) { + AbstractSpan active = tracer.getActive(); + profiler.onActivation( + context.getTraceContext(), active != null ? active.getTraceContext() : null); + } + } + + @Override + public void afterDeactivate(AbstractSpan deactivatedContext) { + if (deactivatedContext.isSampled() && !ThreadUtil.isVirtual(Thread.currentThread())) { + AbstractSpan active = tracer.getActive(); + profiler.onDeactivation( + deactivatedContext.getTraceContext(), active != null ? active.getTraceContext() : null); + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingConfiguration.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingConfiguration.java new file mode 100644 index 000000000..15eaf90a9 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingConfiguration.java @@ -0,0 +1,266 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import static co.elastic.apm.agent.tracer.configuration.RangeValidator.isInRange; +import static co.elastic.apm.agent.tracer.configuration.RangeValidator.min; + +import co.elastic.apm.agent.common.util.WildcardMatcher; +import co.elastic.apm.agent.tracer.configuration.ListValueConverter; +import co.elastic.apm.agent.tracer.configuration.TimeDuration; +import co.elastic.apm.agent.tracer.configuration.TimeDurationValueConverter; +import co.elastic.apm.agent.tracer.configuration.WildcardMatcherValueConverter; +import java.util.Arrays; +import java.util.List; +import org.stagemonitor.configuration.ConfigurationOption; +import org.stagemonitor.configuration.ConfigurationOptionProvider; + +public class ProfilingConfiguration extends ConfigurationOptionProvider { + + private static final String PROFILING_CATEGORY = "Profiling"; + + private final ConfigurationOption profilingEnabled = + ConfigurationOption.booleanOption() + .key("profiling_inferred_spans_enabled") + .configurationCategory(PROFILING_CATEGORY) + .description( + "Set to `true` to make the agent create spans for method executions based on\n" + + "https://github.com/jvm-profiling-tools/async-profiler[async-profiler], a sampling aka statistical profiler.\n" + + "\n" + + "Due to the nature of how sampling profilers work,\n" + + "the duration of the inferred spans are not exact, but only estimations.\n" + + "The <> lets you fine tune the trade-off between accuracy and overhead.\n" + + "\n" + + "The inferred spans are created after a profiling session has ended.\n" + + "This means there is a delay between the regular and the inferred spans being visible in the UI.\n" + + "\n" + + "Only platform threads are supported. Virtual threads are not supported and will not be profiled.\n" + + "\n" + + "NOTE: This feature is not available on Windows and on OpenJ9") + .dynamic(true) + .tags("added[1.15.0]", "experimental") + .buildWithDefault(false); + + private final ConfigurationOption profilerLoggingEnabled = + ConfigurationOption.booleanOption() + .key("profiling_inferred_spans_logging_enabled") + .configurationCategory(PROFILING_CATEGORY) + .description( + "By default, async profiler prints warning messages about missing JVM symbols to standard output. \n" + + "Set this option to `true` to suppress such messages") + .dynamic(true) + .tags("added[1.37.0]") + .buildWithDefault(true); + + private final ConfigurationOption backupDiagnosticFiles = + ConfigurationOption.booleanOption() + .key("profiling_inferred_spans_backup_diagnostic_files") + .configurationCategory(PROFILING_CATEGORY) + .dynamic(true) + .tags("added[1.15.0]", "internal") + .buildWithDefault(false); + + private final ConfigurationOption asyncProfilerSafeMode = + ConfigurationOption.integerOption() + .key("async_profiler_safe_mode") + .configurationCategory(PROFILING_CATEGORY) + .dynamic(false) + .description( + "Can be used for analysis: the Async Profiler's area that deals with recovering stack trace frames \n" + + "is known to be sensitive in some systems. It is used as a bit mask using values are between 0 and 31, \n" + + "where 0 enables all recovery attempts and 31 disables all five (corresponding 1, 2, 4, 8 and 16).") + .tags("internal") + .buildWithDefault(0); + + private final ConfigurationOption postProcessingEnabled = + ConfigurationOption.booleanOption() + .key("profiling_inferred_spans_post_processing_enabled") + .configurationCategory(PROFILING_CATEGORY) + .dynamic(true) + .description( + "Can be used to test the effect of the async-profiler in isolation from the agent's post-processing.") + .tags("added[1.18.0]", "internal") + .buildWithDefault(true); + + private final ConfigurationOption samplingInterval = + TimeDurationValueConverter.durationOption("ms") + .key("profiling_inferred_spans_sampling_interval") + .configurationCategory(PROFILING_CATEGORY) + .dynamic(true) + .description( + "The frequency at which stack traces are gathered within a profiling session.\n" + + "The lower you set it, the more accurate the durations will be.\n" + + "This comes at the expense of higher overhead and more spans for potentially irrelevant operations.\n" + + "The minimal duration of a profiling-inferred span is the same as the value of this setting.") + .addValidator(isInRange(TimeDuration.of("1ms"), TimeDuration.of("1s"))) + .tags("added[1.15.0]") + .buildWithDefault(TimeDuration.of("50ms")); + + private final ConfigurationOption inferredSpansMinDuration = + TimeDurationValueConverter.durationOption("ms") + .key("profiling_inferred_spans_min_duration") + .configurationCategory(PROFILING_CATEGORY) + .dynamic(true) + .description( + "The minimum duration of an inferred span.\n" + + "Note that the min duration is also implicitly set by the sampling interval.\n" + + "However, increasing the sampling interval also decreases the accuracy of the duration of inferred spans.") + .tags("added[1.15.0]") + .addValidator(min(TimeDuration.of("0ms"))) + .buildWithDefault(TimeDuration.of("0ms")); + + private final ConfigurationOption> includedClasses = + ConfigurationOption.builder( + new ListValueConverter<>(new WildcardMatcherValueConverter()), List.class) + .key("profiling_inferred_spans_included_classes") + .configurationCategory(PROFILING_CATEGORY) + .description( + "If set, the agent will only create inferred spans for methods which match this list.\n" + + "Setting a value may slightly reduce overhead and can reduce clutter by only creating spans for the classes you are interested in.\n" + + "Example: `org.example.myapp.*`\n" + + "\n" + + WildcardMatcher.DOCUMENTATION) + .dynamic(true) + .tags("added[1.15.0]") + .buildWithDefault(WildcardMatcher.matchAllList()); + + private final ConfigurationOption> excludedClasses = + ConfigurationOption.builder( + new ListValueConverter<>(new WildcardMatcherValueConverter()), List.class) + .key("profiling_inferred_spans_excluded_classes") + .configurationCategory(PROFILING_CATEGORY) + .description( + "Excludes classes for which no profiler-inferred spans should be created.\n" + + "\n" + + WildcardMatcher.DOCUMENTATION) + .dynamic(true) + .tags("added[1.15.0]") + .buildWithDefault( + Arrays.asList( + WildcardMatcher.caseSensitiveMatcher("java.*"), + WildcardMatcher.caseSensitiveMatcher("javax.*"), + WildcardMatcher.caseSensitiveMatcher("sun.*"), + WildcardMatcher.caseSensitiveMatcher("com.sun.*"), + WildcardMatcher.caseSensitiveMatcher("jdk.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.tomcat.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.catalina.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.coyote.*"), + WildcardMatcher.caseSensitiveMatcher("org.jboss.as.*"), + WildcardMatcher.caseSensitiveMatcher("org.glassfish.*"), + WildcardMatcher.caseSensitiveMatcher("org.eclipse.jetty.*"), + WildcardMatcher.caseSensitiveMatcher("com.ibm.websphere.*"), + WildcardMatcher.caseSensitiveMatcher("io.undertow.*"))); + + private final ConfigurationOption profilerInterval = + TimeDurationValueConverter.durationOption("s") + .key("profiling_inferred_spans_interval") + .description("The interval at which profiling sessions should be started.") + .configurationCategory(PROFILING_CATEGORY) + .addValidator(min(TimeDuration.of("0ms"))) + .dynamic(true) + .tags("added[1.15.0]", "internal") + .buildWithDefault(TimeDuration.of("5s")); + + private final ConfigurationOption profilingDuration = + TimeDurationValueConverter.durationOption("s") + .key("profiling_inferred_spans_duration") + .description( + "The duration of a profiling session.\n" + + "For sampled transactions which fall within a profiling session (they start after and end before the session),\n" + + "so-called inferred spans will be created.\n" + + "They appear in the trace waterfall view like regular spans.\n" + + "\n" + + "NOTE: It is not recommended to set much higher durations as it may fill the activation events file and async-profiler's frame buffer.\n" + + "Warnings will be logged if the activation events file is full.\n" + + "If you want to have more profiling coverage, try decreasing <>.") + .configurationCategory(PROFILING_CATEGORY) + .dynamic(true) + .addValidator(isInRange(TimeDuration.of("1s"), TimeDuration.of("30s"))) + .tags("added[1.15.0]", "internal") + .buildWithDefault(TimeDuration.of("5s")); + + private final ConfigurationOption profilerLibDirectory = + ConfigurationOption.stringOption() + .key("profiling_inferred_spans_lib_directory") + .description( + "Profiling requires that the https://github.com/jvm-profiling-tools/async-profiler[async-profiler] shared library " + + "is exported to a temporary location and loaded by the JVM.\n" + + "The partition backing this location must be executable, however in some server-hardened environments, " + + "`noexec` may be set on the standard `/tmp` partition, leading to `java.lang.UnsatisfiedLinkError` errors.\n" + + "Set this property to an alternative directory (e.g. `/var/tmp`) to resolve this.\n" + + "If unset, the value of the `java.io.tmpdir` system property will be used.") + .configurationCategory(PROFILING_CATEGORY) + .dynamic(false) + .tags("added[1.18.0]") + .build(); + + public boolean isProfilingEnabled() { + return profilingEnabled.get(); + } + + public boolean isProfilingLoggingEnabled() { + return profilerLoggingEnabled.get(); + } + + public int getAsyncProfilerSafeMode() { + return asyncProfilerSafeMode.get(); + } + + public TimeDuration getSamplingInterval() { + return samplingInterval.get(); + } + + public TimeDuration getInferredSpansMinDuration() { + return inferredSpansMinDuration.get(); + } + + public List getIncludedClasses() { + return includedClasses.get(); + } + + public List getExcludedClasses() { + return excludedClasses.get(); + } + + public TimeDuration getProfilingInterval() { + return profilerInterval.get(); + } + + public TimeDuration getProfilingDuration() { + return profilingDuration.get(); + } + + public boolean isNonStopProfiling() { + return getProfilingDuration().getMillis() >= getProfilingInterval().getMillis(); + } + + public boolean isBackupDiagnosticFiles() { + return backupDiagnosticFiles.get(); + } + + public String getProfilerLibDirectory() { + return profilerLibDirectory.isDefault() + ? System.getProperty("java.io.tmpdir") + : profilerLibDirectory.get(); + } + + public boolean isPostProcessingEnabled() { + return postProcessingEnabled.get(); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingFactory.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingFactory.java new file mode 100644 index 000000000..4c2d58a8e --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingFactory.java @@ -0,0 +1,55 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import co.elastic.apm.agent.context.AbstractLifecycleListener; +import co.elastic.apm.agent.impl.ElasticApmTracer; + +public class ProfilingFactory extends AbstractLifecycleListener { + + private final SamplingProfiler profiler; + private final NanoClock nanoClock; + + public ProfilingFactory(ElasticApmTracer tracer) { + boolean envTest = false; + // in unit tests, where assertions are enabled, this envTest is true + assert envTest = true; + nanoClock = envTest ? new FixedNanoClock() : new SystemNanoClock(); + profiler = new SamplingProfiler(tracer, nanoClock); + } + + @Override + public void start(ElasticApmTracer tracer) { + profiler.start(tracer); + tracer.registerSpanListener(new ProfilingActivationListener(tracer, profiler)); + } + + @Override + public void stop() throws Exception { + profiler.stop(); + } + + public SamplingProfiler getProfiler() { + return profiler; + } + + public NanoClock getNanoClock() { + return nanoClock; + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java new file mode 100644 index 000000000..54c96d377 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java @@ -0,0 +1,1126 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import static java.nio.file.StandardOpenOption.READ; +import static java.nio.file.StandardOpenOption.WRITE; + +import co.elastic.apm.agent.common.util.WildcardMatcher; +import co.elastic.apm.agent.context.AbstractLifecycleListener; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.StackFrame; +import co.elastic.apm.agent.impl.transaction.TraceContext; +import co.elastic.apm.agent.profiler.asyncprofiler.AsyncProfiler; +import co.elastic.apm.agent.profiler.asyncprofiler.JfrParser; +import co.elastic.apm.agent.profiler.collections.Long2ObjectHashMap; +import co.elastic.apm.agent.sdk.internal.util.ExecutorUtils; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import co.elastic.apm.agent.tracer.configuration.CoreConfiguration; +import co.elastic.apm.agent.tracer.configuration.TimeDuration; +import co.elastic.apm.agent.tracer.pooling.Allocator; +import co.elastic.apm.agent.tracer.pooling.ObjectPool; +import com.lmax.disruptor.EventFactory; +import com.lmax.disruptor.EventPoller; +import com.lmax.disruptor.EventTranslatorTwoArg; +import com.lmax.disruptor.RingBuffer; +import com.lmax.disruptor.Sequence; +import com.lmax.disruptor.SequenceBarrier; +import com.lmax.disruptor.WaitStrategy; +import java.io.File; +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import javax.annotation.Nullable; + +/** + * Correlates {@link ActivationEvent}s with {@link StackFrame}s which are recorded by {@link + * AsyncProfiler}, a native {@code + * AsyncGetCallTree}-based (and therefore non + * safepoint-biased) JVMTI agent. + * + *

Recording of {@link ActivationEvent}s: + * + *

The {@link #onActivation} and {@link #onDeactivation} methods are called by {@link + * ProfilingActivationListener} which register an {@link ActivationEvent} to a {@linkplain + * #eventBuffer ring buffer} whenever a {@link Span} gets {@link Span#activate()}d or {@link + * Span#deactivate()}d while a {@linkplain #profilingSessionOngoing profiling session is ongoing}. A + * background thread consumes the {@link ActivationEvent}s and writes them to a {@linkplain + * #activationEventsBuffer direct buffer} which is flushed to a {@linkplain + * #activationEventsFileChannel file}. That is necessary because within a profiling session (which + * lasts 10s by default) there may be many more {@link ActivationEvent}s than the ring buffer {@link + * #RING_BUFFER_SIZE can hold}. The file can hold {@link #ACTIVATION_EVENTS_IN_FILE} events and each + * is {@link ActivationEvent#SERIALIZED_SIZE} in size. This process is completely garbage free + * thanks to the {@link RingBuffer} acting as an object pool for {@link ActivationEvent}s. + * + *

Recording stack traces: + * + *

The same background thread that processes the {@link ActivationEvent}s starts the wall clock + * profiler of async-profiler via {@link AsyncProfiler#execute(String)}. After the {@link + * ProfilingConfiguration#getProfilingDuration()} is over it stops the profiling and starts + * processing the JFR file created by async-profiler with {@link JfrParser}. + * + *

Correlating {@link ActivationEvent}s with the traces recorded by {@link AsyncProfiler}: + * + *

After both the JFR file and the file containing the {@link ActivationEvent}s have been + * written, it's now time to process them in tandem by correlating based on thread ids and + * timestamps. The result of this correlation, performed by {@link #processTraces}, are {@link + * CallTree}s which are created for each thread which has seen an {@linkplain Span#activate() + * activation} and at least one stack trace. Once {@linkplain + * ActivationEvent#handleDeactivationEvent(SamplingProfiler) handling the deactivation event} of the + * root span in a thread (after which {@link ElasticApmTracer#getActive()} would return {@code + * null}), the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext) + * converted into regular spans}. + * + *

Overall, the allocation rate does not depend on the number of {@link ActivationEvent}s but + * only on {@link ProfilingConfiguration#getProfilingInterval()} and {@link + * ProfilingConfiguration#getSamplingInterval()}. Having said that, there are some optimizations so + * that the JFR file is not processed at all if there have not been any {@link ActivationEvent} in a + * given profiling session. Also, only if there's a {@link CallTree.Root} for a {@link + * StackTraceEvent}, we will {@link JfrParser#resolveStackTrace(long, boolean, List, int) resolve + * the full stack trace}. + */ +public class SamplingProfiler extends AbstractLifecycleListener implements Runnable { + + private static final Logger logger = LoggerFactory.getLogger(SamplingProfiler.class); + private static final int ACTIVATION_EVENTS_IN_FILE = 1_000_000; + private static final int MAX_STACK_DEPTH = 256; + private static final int PRE_ALLOCATE_ACTIVATION_EVENTS_FILE_MB = 10; + private static final int MAX_ACTIVATION_EVENTS_FILE_SIZE = + ACTIVATION_EVENTS_IN_FILE * ActivationEvent.SERIALIZED_SIZE; + private static final int ACTIVATION_EVENTS_BUFFER_SIZE = + ActivationEvent.SERIALIZED_SIZE * 4 * 1024; + private final EventTranslatorTwoArg + ACTIVATION_EVENT_TRANSLATOR = + new EventTranslatorTwoArg() { + @Override + public void translateTo( + ActivationEvent event, + long sequence, + TraceContext active, + TraceContext previouslyActive) { + event.activation( + active, Thread.currentThread().getId(), previouslyActive, nanoClock.nanoTime()); + } + }; + private final EventTranslatorTwoArg + DEACTIVATION_EVENT_TRANSLATOR = + new EventTranslatorTwoArg() { + @Override + public void translateTo( + ActivationEvent event, + long sequence, + TraceContext active, + TraceContext previouslyActive) { + event.deactivation( + active, Thread.currentThread().getId(), previouslyActive, nanoClock.nanoTime()); + } + }; + // sizeof(ActivationEvent) is 176B so the ring buffer should be around 880KiB + static final int RING_BUFFER_SIZE = 4 * 1024; + + private final ProfilingConfiguration config; + private final CoreConfiguration coreConfig; + private final ScheduledExecutorService scheduler; + private final Long2ObjectHashMap profiledThreads = new Long2ObjectHashMap<>(); + private final RingBuffer eventBuffer; + private volatile boolean profilingSessionOngoing = false; + private final Sequence sequence; + private final ElasticApmTracer tracer; + private final NanoClock nanoClock; + private final ObjectPool rootPool; + private final ThreadMatcher threadMatcher = new ThreadMatcher(); + private final EventPoller poller; + @Nullable private File jfrFile; + private boolean canDeleteJfrFile; + private final WriteActivationEventToFileHandler writeActivationEventToFileHandler = + new WriteActivationEventToFileHandler(); + @Nullable private JfrParser jfrParser; + private volatile int profilingSessions; + + private final ByteBuffer activationEventsBuffer; + + /** + * Used to efficiently write {@link #activationEventsBuffer} via {@link + * FileChannel#write(ByteBuffer)} + */ + @Nullable private File activationEventsFile; + + private boolean canDeleteActivationEventsFile; + + @Nullable private FileChannel activationEventsFileChannel; + private final ObjectPool callTreePool; + private final TraceContext contextForLogging; + + private boolean previouslyEnabled = false; + + /** + * Creates a sampling profiler using temporary files + * + * @param tracer tracer + * @param nanoClock clock + */ + public SamplingProfiler(ElasticApmTracer tracer, NanoClock nanoClock) { + this(tracer, nanoClock, null, null); + } + + /** + * Creates a sampling profiler, optionally relying on existing files. + * + *

This constructor is most likely used for tests that rely on a known set of files + * + * @param tracer tracer + * @param nanoClock clock + * @param activationEventsFile activation events file, if {@literal null} a temp file will be used + * @param jfrFile java flight recorder file, if {@literal null} a temp file will be used instead + */ + public SamplingProfiler( + final ElasticApmTracer tracer, + NanoClock nanoClock, + @Nullable File activationEventsFile, + @Nullable File jfrFile) { + this.tracer = tracer; + this.config = tracer.getConfig(ProfilingConfiguration.class); + this.coreConfig = tracer.getConfig(CoreConfiguration.class); + this.scheduler = ExecutorUtils.createSingleThreadSchedulingDaemonPool("sampling-profiler"); + this.nanoClock = nanoClock; + this.eventBuffer = createRingBuffer(); + this.sequence = new Sequence(); + // tells the ring buffer to not override slots which have not been read yet + this.eventBuffer.addGatingSequences(sequence); + this.poller = eventBuffer.newPoller(); + contextForLogging = TraceContext.with64BitId(tracer); + this.callTreePool = + tracer + .getObjectPoolFactory() + .createRecyclableObjectPool( + 2 * 1024, + new Allocator() { + @Override + public CallTree createInstance() { + return new CallTree(); + } + }); + // call tree roots are pooled so that fast activations/deactivations with no associated stack + // traces don't cause allocations + this.rootPool = + tracer + .getObjectPoolFactory() + .createRecyclableObjectPool( + 512, + new Allocator() { + @Override + public CallTree.Root createInstance() { + return new CallTree.Root(tracer); + } + }); + this.jfrFile = jfrFile; + activationEventsBuffer = ByteBuffer.allocateDirect(ACTIVATION_EVENTS_BUFFER_SIZE); + this.activationEventsFile = activationEventsFile; + } + + /** + * For testing only! This method must only be called in tests and some period after activation / + * deactivation events, as otherwise it is racy. + * + * @param thread the Thread to check. + * @return true, if profiling is active for the given thread. + */ + boolean isProfilingActiveOnThread(Thread thread) { + return profiledThreads.containsKey(thread.getId()); + } + + private synchronized void createFilesIfRequired() throws IOException { + if (jfrFile == null || !jfrFile.exists()) { + jfrFile = File.createTempFile("apm-traces-", ".jfr"); + jfrFile.deleteOnExit(); + canDeleteJfrFile = true; + } + if (activationEventsFile == null || !activationEventsFile.exists()) { + activationEventsFile = File.createTempFile("apm-activation-events-", ".bin"); + activationEventsFile.deleteOnExit(); + canDeleteActivationEventsFile = true; + } + if (activationEventsFileChannel == null || !activationEventsFileChannel.isOpen()) { + activationEventsFileChannel = + FileChannel.open( + activationEventsFile.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE); + } + if (activationEventsFileChannel.size() == 0) { + preAllocate(activationEventsFileChannel, PRE_ALLOCATE_ACTIVATION_EVENTS_FILE_MB); + } + } + + // visible for benchmarks + public void skipToEndOfActivationEventsFile() throws IOException { + activationEventsFileChannel.position(activationEventsFileChannel.size()); + } + + /** + * Makes sure that the first blocks of the file are contiguous to provide fast sequential access + */ + private static void preAllocate(FileChannel channel, int mb) throws IOException { + long initialPos = channel.position(); + ByteBuffer oneKb = ByteBuffer.allocate(1024); + for (int i = 0; i < mb * 1024; i++) { + channel.write(oneKb); + ((Buffer) oneKb).clear(); + } + channel.position(initialPos); + } + + private RingBuffer createRingBuffer() { + return RingBuffer.createMultiProducer( + new EventFactory() { + @Override + public ActivationEvent newInstance() { + return new ActivationEvent(); + } + }, + RING_BUFFER_SIZE, + new NoWaitStrategy()); + } + + /** + * Called whenever a span is activated. + * + *

This and {@link #onDeactivation} are the only methods which are executed in a multi-threaded + * context. + * + * @param activeSpan the span which is about to be activated + * @param previouslyActive the span which has previously been activated + * @return {@code true}, if the event could be processed, {@code false} if the internal event + * queue is full which means the event has been discarded + */ + public boolean onActivation(TraceContext activeSpan, @Nullable TraceContext previouslyActive) { + if (profilingSessionOngoing) { + if (previouslyActive == null) { + AsyncProfiler.getInstance( + config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()) + .enableProfilingCurrentThread(); + } + boolean success = + eventBuffer.tryPublishEvent(ACTIVATION_EVENT_TRANSLATOR, activeSpan, previouslyActive); + if (!success && logger.isDebugEnabled()) { + logger.debug("Could not add activation event to ring buffer as no slots are available"); + } + return success; + } + return false; + } + + /** + * Called whenever a span is deactivated. + * + *

This and {@link #onActivation} are the only methods which are executed in a multi-threaded + * context. + * + * @param activeSpan the span which is about to be activated + * @param previouslyActive the span which has previously been activated + * @return {@code true}, if the event could be processed, {@code false} if the internal event + * queue is full which means the event has been discarded + */ + public boolean onDeactivation(TraceContext activeSpan, @Nullable TraceContext previouslyActive) { + if (profilingSessionOngoing) { + if (previouslyActive == null) { + AsyncProfiler.getInstance( + config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()) + .disableProfilingCurrentThread(); + } + boolean success = + eventBuffer.tryPublishEvent(DEACTIVATION_EVENT_TRANSLATOR, activeSpan, previouslyActive); + if (!success && logger.isDebugEnabled()) { + logger.debug("Could not add deactivation event to ring buffer as no slots are available"); + } + return success; + } + return false; + } + + @Override + public void run() { + + boolean enabled = config.isProfilingEnabled() && tracer.isRunning(); + boolean hasBeenDisabled = previouslyEnabled && !enabled; + previouslyEnabled = enabled; + + if (!enabled) { + if (jfrParser != null) { + jfrParser = null; + } + if (!scheduler.isShutdown()) { + scheduler.schedule(this, config.getProfilingInterval().getMillis(), TimeUnit.MILLISECONDS); + } + + if (hasBeenDisabled) { + // only clear when going from enabled -> disabled state + try { + clear(); + } catch (Throwable throwable) { + logger.error("Error while trying to clear profiler constructs", throwable); + } + } + + return; + } + + // lazily create temporary files + try { + createFilesIfRequired(); + } catch (IOException e) { + logger.error("unable to initialize profiling files", e); + return; + } + + TimeDuration profilingDuration = config.getProfilingDuration(); + boolean postProcessingEnabled = config.isPostProcessingEnabled(); + + setProfilingSessionOngoing(postProcessingEnabled); + + if (postProcessingEnabled) { + logger.debug("Start full profiling session (async-profiler and agent processing)"); + } else { + logger.debug("Start async-profiler profiling session"); + } + try { + profile(profilingDuration); + } catch (Throwable t) { + setProfilingSessionOngoing(false); + logger.error("Stopping profiler", t); + return; + } + logger.debug("End profiling session"); + + boolean interrupted = Thread.currentThread().isInterrupted(); + boolean continueProfilingSession = + config.isNonStopProfiling() + && !interrupted + && config.isProfilingEnabled() + && postProcessingEnabled; + setProfilingSessionOngoing(continueProfilingSession); + + if (!interrupted && !scheduler.isShutdown()) { + long delay = config.getProfilingInterval().getMillis() - profilingDuration.getMillis(); + scheduler.schedule(this, delay, TimeUnit.MILLISECONDS); + } + } + + private void profile(TimeDuration profilingDuration) throws Exception { + AsyncProfiler asyncProfiler = + AsyncProfiler.getInstance( + config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()); + try { + String startCommand = createStartCommand(); + String startMessage = asyncProfiler.execute(startCommand); + logger.debug(startMessage); + if (!profiledThreads.isEmpty()) { + restoreFilterState(asyncProfiler); + } + // Doesn't need to be atomic as this field is being updated only by a single thread + //noinspection NonAtomicOperationOnVolatileField + profilingSessions++; + + // When post-processing is disabled activation events are ignored, but we still need to invoke + // this method + // as it is the one enforcing the sampling session duration. As a side effect it will also + // consume + // residual activation events if post-processing is disabled dynamically + consumeActivationEventsFromRingBufferAndWriteToFile(profilingDuration); + + String stopMessage = asyncProfiler.execute("stop"); + logger.debug(stopMessage); + + // When post-processing is disabled, jfr file will not be parsed and the heavy processing will + // not occur + // as this method aborts when no activation events are buffered + processTraces(); + } catch (InterruptedException | ClosedByInterruptException e) { + try { + asyncProfiler.stop(); + } catch (IllegalStateException ignore) { + } + Thread.currentThread().interrupt(); + } + } + + String createStartCommand() { + StringBuilder startCommand = + new StringBuilder("start,jfr,event=wall,cstack=n,interval=") + .append(config.getSamplingInterval().getMillis()) + .append("ms,filter,file=") + .append(jfrFile) + .append(",safemode=") + .append(config.getAsyncProfilerSafeMode()); + if (!config.isProfilingLoggingEnabled()) { + startCommand.append(",log=none"); + } + return startCommand.toString(); + } + + /** + * When doing continuous profiling (interval=duration), we have to tell async-profiler which + * threads it should profile after re-starting it. + */ + private void restoreFilterState(AsyncProfiler asyncProfiler) { + threadMatcher.forEachThread( + new ThreadMatcher.NonCapturingPredicate.KeySet>() { + @Override + public boolean test(Thread thread, Long2ObjectHashMap.KeySet profiledThreads) { + return profiledThreads.contains(thread.getId()); + } + }, + profiledThreads.keySet(), + new ThreadMatcher.NonCapturingConsumer() { + @Override + public void accept(Thread thread, AsyncProfiler asyncProfiler) { + asyncProfiler.enableProfilingThread(thread); + } + }, + asyncProfiler); + } + + private void consumeActivationEventsFromRingBufferAndWriteToFile(TimeDuration profilingDuration) + throws Exception { + resetActivationEventBuffer(); + long threshold = System.currentTimeMillis() + profilingDuration.getMillis(); + long initialSleep = 100_000; + long maxSleep = 10_000_000; + long sleep = initialSleep; + while (System.currentTimeMillis() < threshold && !Thread.currentThread().isInterrupted()) { + if (activationEventsFileChannel.position() < MAX_ACTIVATION_EVENTS_FILE_SIZE) { + EventPoller.PollState poll = consumeActivationEventsFromRingBufferAndWriteToFile(); + if (poll == EventPoller.PollState.PROCESSING) { + sleep = initialSleep; + // don't sleep, after consuming the events there might be new ones in the ring buffer + } else { + if (sleep < maxSleep) { + sleep *= 2; + } + LockSupport.parkNanos(sleep); + } + } else { + logger.warn("The activation events file is full. Try lowering the profiling_duration."); + // the file is full, sleep the rest of the profilingDuration + Thread.sleep(Math.max(0, threshold - System.currentTimeMillis())); + } + } + } + + EventPoller.PollState consumeActivationEventsFromRingBufferAndWriteToFile() throws Exception { + createFilesIfRequired(); + return poller.poll(writeActivationEventToFileHandler); + } + + public void processTraces() throws IOException { + if (jfrParser == null) { + jfrParser = new JfrParser(); + } + if (Thread.currentThread().isInterrupted()) { + return; + } + createFilesIfRequired(); + + long eof = startProcessingActivationEventsFile(); + if (eof == 0 && activationEventsBuffer.limit() == 0 && profiledThreads.isEmpty()) { + logger.debug("No activation events during this period. Skip processing stack traces."); + return; + } + long start = System.nanoTime(); + List excludedClasses = config.getExcludedClasses(); + List includedClasses = config.getIncludedClasses(); + if (config.isBackupDiagnosticFiles()) { + backupDiagnosticFiles(eof); + } + try { + jfrParser.parse(jfrFile, excludedClasses, includedClasses); + final List stackTraceEvents = getSortedStackTraceEvents(jfrParser); + if (logger.isDebugEnabled()) { + logger.debug("Processing {} stack traces", stackTraceEvents.size()); + } + List stackFrames = new ArrayList<>(); + ElasticApmTracer tracer = this.tracer; + ActivationEvent event = new ActivationEvent(); + long inferredSpansMinDuration = getInferredSpansMinDurationNs(); + for (StackTraceEvent stackTrace : stackTraceEvents) { + processActivationEventsUpTo(stackTrace.nanoTime, event, eof); + CallTree.Root root = profiledThreads.get(stackTrace.threadId); + if (root != null) { + jfrParser.resolveStackTrace(stackTrace.stackTraceId, true, stackFrames, MAX_STACK_DEPTH); + if (stackFrames.size() == MAX_STACK_DEPTH) { + logger.debug( + "Max stack depth reached. Set profiling_included_classes or profiling_excluded_classes."); + } + // stack frames may not contain any Java frames + // see + // https://github.com/jvm-profiling-tools/async-profiler/issues/271#issuecomment-582430233 + if (!stackFrames.isEmpty()) { + try { + root.addStackTrace( + tracer, stackFrames, stackTrace.nanoTime, callTreePool, inferredSpansMinDuration); + } catch (Exception e) { + logger.warn( + "Removing call tree for thread {} because of exception while adding a stack trace: {} {}", + stackTrace.threadId, + e.getClass(), + e.getMessage()); + logger.debug(e.getMessage(), e); + profiledThreads.remove(stackTrace.threadId); + } + } + } + stackFrames.clear(); + } + // process all activation events that happened after the last stack trace event + // otherwise we may miss root deactivations + processActivationEventsUpTo(System.nanoTime(), event, eof); + } finally { + if (logger.isDebugEnabled()) { + logger.debug("Processing traces took {}µs", (System.nanoTime() - start) / 1000); + } + jfrParser.resetState(); + resetActivationEventBuffer(); + } + } + + private void backupDiagnosticFiles(long eof) throws IOException { + String now = String.format("%tFT% 0) { + activationEventsFileChannel.transferTo(0, eof, activationsFile); + } else { + int position = activationEventsBuffer.position(); + activationsFile.write(activationEventsBuffer); + activationEventsBuffer.position(position); + } + } + Files.copy(jfrFile.toPath(), profilerDir.resolve(now + "-traces.jfr")); + } + + private long getInferredSpansMinDurationNs() { + return Math.max( + config.getInferredSpansMinDuration().getMillis(), + coreConfig.getSpanMinDuration().getMillis()) + * 1_000_000; + } + + /** + * Returns stack trace events of relevant threads sorted by timestamp. The events in the JFR file + * are not in order. Even for the same thread, a more recent event might come before an older + * event. In order to be able to correlate stack trace events and activation events, both need to + * be in order. + * + *

Returns only events for threads where at least one activation happened (because only those + * are profiled by async-profiler) + */ + private List getSortedStackTraceEvents(JfrParser jfrParser) throws IOException { + final List stackTraceEvents = new ArrayList<>(); + jfrParser.consumeStackTraces( + new JfrParser.StackTraceConsumer() { + @Override + public void onCallTree(long threadId, long stackTraceId, long nanoTime) { + stackTraceEvents.add(new StackTraceEvent(nanoTime, stackTraceId, threadId)); + } + }); + Collections.sort(stackTraceEvents); + return stackTraceEvents; + } + + void processActivationEventsUpTo(long timestamp, long eof) throws IOException { + processActivationEventsUpTo(timestamp, new ActivationEvent(), eof); + } + + public void processActivationEventsUpTo(long timestamp, ActivationEvent event, long eof) + throws IOException { + FileChannel activationEventsFileChannel = this.activationEventsFileChannel; + ByteBuffer buf = activationEventsBuffer; + long previousTimestamp = 0; + while (buf.hasRemaining() || activationEventsFileChannel.position() < eof) { + if (!buf.hasRemaining()) { + readActivationEventsToBuffer(activationEventsFileChannel, eof, buf); + } + long eventTimestamp = peekLong(buf); + if (eventTimestamp < previousTimestamp && logger.isDebugEnabled()) { + logger.debug( + "Timestamp of current activation event ({}) is lower than the one from the previous event ({})", + eventTimestamp, + previousTimestamp); + } + previousTimestamp = eventTimestamp; + if (eventTimestamp <= timestamp) { + event.deserialize(buf); + try { + event.handle(this); + } catch (Exception e) { + logger.warn( + "Removing call tree for thread {} because of exception while handling activation event: {} {}", + event.threadId, + e.getClass(), + e.getMessage()); + logger.debug(e.getMessage(), e); + profiledThreads.remove(event.threadId); + } + } else { + return; + } + } + } + + private void readActivationEventsToBuffer( + FileChannel activationEventsFileChannel, long eof, ByteBuffer byteBuffer) throws IOException { + Buffer buf = byteBuffer; + buf.clear(); + long remaining = eof - activationEventsFileChannel.position(); + activationEventsFileChannel.read(byteBuffer); + buf.flip(); + if (remaining < buf.capacity()) { + buf.limit((int) remaining); + } + } + + private static long peekLong(ByteBuffer buf) { + int pos = buf.position(); + try { + return buf.getLong(); + } finally { + ((Buffer) buf).position(pos); + } + } + + public void resetActivationEventBuffer() throws IOException { + ((Buffer) activationEventsBuffer).clear(); + if (activationEventsFileChannel != null && activationEventsFileChannel.isOpen()) { + activationEventsFileChannel.position(0L); + } + } + + private void flushActivationEvents() throws IOException { + if (activationEventsBuffer.position() > 0) { + ((Buffer) activationEventsBuffer).flip(); + activationEventsFileChannel.write(activationEventsBuffer); + ((Buffer) activationEventsBuffer).clear(); + } + } + + long startProcessingActivationEventsFile() throws IOException { + Buffer activationEventsBuffer = this.activationEventsBuffer; + if (activationEventsFileChannel.position() > 0) { + flushActivationEvents(); + activationEventsBuffer.limit(0); + } else { + activationEventsBuffer.flip(); + } + long eof = activationEventsFileChannel.position(); + activationEventsFileChannel.position(0); + return eof; + } + + void copyFromFiles(Path activationEvents, Path traces) throws IOException { + createFilesIfRequired(); + + FileChannel otherActivationsChannel = FileChannel.open(activationEvents, READ); + activationEventsFileChannel.transferFrom( + otherActivationsChannel, 0, otherActivationsChannel.size()); + activationEventsFileChannel.position(otherActivationsChannel.size()); + FileChannel otherTracesChannel = FileChannel.open(traces, READ); + FileChannel.open(jfrFile.toPath(), WRITE) + .transferFrom(otherTracesChannel, 0, otherTracesChannel.size()); + } + + @Override + public void start(ElasticApmTracer tracer) { + scheduler.submit(this); + } + + @Override + public void stop() throws Exception { + // cancels/interrupts the profiling thread + // implicitly clears profiled threads + ExecutorUtils.shutdownAndWaitTermination(scheduler); + + if (activationEventsFileChannel != null) { + activationEventsFileChannel.close(); + } + + if (jfrFile != null && canDeleteJfrFile) { + jfrFile.delete(); + } + if (activationEventsFile != null && canDeleteActivationEventsFile) { + activationEventsFile.delete(); + } + } + + void setProfilingSessionOngoing(boolean profilingSessionOngoing) { + this.profilingSessionOngoing = profilingSessionOngoing; + if (!profilingSessionOngoing) { + clearProfiledThreads(); + } else if (!profiledThreads.isEmpty() && logger.isDebugEnabled()) { + logger.debug("Retaining {} call tree roots", profiledThreads.size()); + } + } + + public void clearProfiledThreads() { + for (CallTree.Root root : profiledThreads.values()) { + root.recycle(callTreePool, rootPool); + } + profiledThreads.clear(); + } + + // for testing + CallTree.Root getRoot() { + return profiledThreads.get(Thread.currentThread().getId()); + } + + void clear() throws IOException { + // consume all remaining events from the ring buffer + try { + poller.poll( + new EventPoller.Handler() { + @Override + public boolean onEvent(ActivationEvent event, long sequence, boolean endOfBatch) { + SamplingProfiler.this.sequence.set(sequence); + return true; + } + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + resetActivationEventBuffer(); + profiledThreads.clear(); + callTreePool.clear(); + rootPool.clear(); + } + + int getProfilingSessions() { + return profilingSessions; + } + + // -- + + public static class StackTraceEvent implements Comparable { + private final long nanoTime; + private final long stackTraceId; + private final long threadId; + + private StackTraceEvent(long nanoTime, long stackTraceId, long threadId) { + this.nanoTime = nanoTime; + this.stackTraceId = stackTraceId; + this.threadId = threadId; + } + + public long getThreadId() { + return threadId; + } + + public long getNanoTime() { + return nanoTime; + } + + public long getStackTraceId() { + return stackTraceId; + } + + @Override + public int compareTo(StackTraceEvent o) { + return Long.compare(nanoTime, o.nanoTime); + } + } + + private static class ActivationEvent { + public static final int SERIALIZED_SIZE = + Long.SIZE / Byte.SIZE + + // timestamp + Short.SIZE / Byte.SIZE + + // serviceName index + Short.SIZE / Byte.SIZE + + // serviceVersion index + TraceContext.SERIALIZED_LENGTH + + // traceContextBuffer + TraceContext.SERIALIZED_LENGTH + + // previousContextBuffer + 1 + + // rootContext + Long.SIZE / Byte.SIZE + + // threadId + 1; // activation + + private static final Map serviceNameMap = new HashMap<>(); + private static final Map serviceNameBackMap = new HashMap<>(); + + private static final Map serviceVersionMap = new HashMap<>(); + private static final Map serviceVersionBackMap = new HashMap<>(); + + private long timestamp; + @Nullable private String serviceName; + @Nullable private String serviceVersion; + private byte[] traceContextBuffer = new byte[TraceContext.SERIALIZED_LENGTH]; + private byte[] previousContextBuffer = new byte[TraceContext.SERIALIZED_LENGTH]; + private boolean rootContext; + private long threadId; + private boolean activation; + + public void activation( + TraceContext context, + long threadId, + @Nullable TraceContext previousContext, + long nanoTime) { + set(context, threadId, true, previousContext != null ? previousContext : null, nanoTime); + } + + public void deactivation( + TraceContext context, + long threadId, + @Nullable TraceContext previousContext, + long nanoTime) { + set(context, threadId, false, previousContext != null ? previousContext : null, nanoTime); + } + + private void set( + TraceContext traceContext, + long threadId, + boolean activation, + @Nullable TraceContext previousContext, + long nanoTime) { + traceContext.serialize(traceContextBuffer); + this.threadId = threadId; + this.activation = activation; + this.serviceName = traceContext.getServiceName(); + this.serviceVersion = traceContext.getServiceVersion(); + if (previousContext != null) { + previousContext.serialize(previousContextBuffer); + rootContext = false; + } else { + rootContext = true; + } + this.timestamp = nanoTime; + } + + public void handle(SamplingProfiler samplingProfiler) { + if (logger.isDebugEnabled()) { + logger.debug( + "Handling event timestamp={} root={} threadId={} activation={}", + timestamp, + rootContext, + threadId, + activation); + } + if (activation) { + handleActivationEvent(samplingProfiler); + } else { + handleDeactivationEvent(samplingProfiler); + } + } + + private void handleActivationEvent(SamplingProfiler samplingProfiler) { + if (rootContext) { + startProfiling(samplingProfiler); + } else { + CallTree.Root root = samplingProfiler.profiledThreads.get(threadId); + if (root != null) { + if (logger.isDebugEnabled()) { + logger.debug("Handling activation for thread {}", threadId); + } + root.onActivation(traceContextBuffer, timestamp); + } else if (logger.isDebugEnabled()) { + logger.debug( + "Illegal state when handling activation event for thread {}: no root found for this thread", + threadId); + } + } + } + + private void startProfiling(SamplingProfiler samplingProfiler) { + CallTree.Root root = + CallTree.createRoot( + samplingProfiler.rootPool, + traceContextBuffer, + serviceName, + serviceVersion, + timestamp); + if (logger.isDebugEnabled()) { + logger.debug( + "Create call tree ({}) for thread {}", + deserialize(samplingProfiler, traceContextBuffer), + threadId); + } + + CallTree.Root orphaned = samplingProfiler.profiledThreads.put(threadId, root); + if (orphaned != null) { + if (logger.isDebugEnabled()) { + logger.warn( + "Illegal state when stopping profiling for thread {}: orphaned root", threadId); + } + orphaned.recycle(samplingProfiler.callTreePool, samplingProfiler.rootPool); + } + } + + private TraceContext deserialize(SamplingProfiler samplingProfiler, byte[] traceContextBuffer) { + samplingProfiler.contextForLogging.deserialize(traceContextBuffer, null, null); + return samplingProfiler.contextForLogging; + } + + private void handleDeactivationEvent(SamplingProfiler samplingProfiler) { + if (rootContext) { + stopProfiling(samplingProfiler); + } else { + CallTree.Root root = samplingProfiler.profiledThreads.get(threadId); + if (root != null) { + if (logger.isDebugEnabled()) { + logger.debug("Handling deactivation for thread {}", threadId); + } + root.onDeactivation(traceContextBuffer, previousContextBuffer, timestamp); + } else if (logger.isDebugEnabled()) { + logger.debug( + "Illegal state when handling deactivation event for thread {}: no root found for this thread", + threadId); + } + } + } + + private void stopProfiling(SamplingProfiler samplingProfiler) { + CallTree.Root callTree = samplingProfiler.profiledThreads.get(threadId); + if (callTree != null && callTree.getRootContext().traceIdAndIdEquals(traceContextBuffer)) { + if (logger.isDebugEnabled()) { + logger.debug( + "End call tree ({}) for thread {}", + deserialize(samplingProfiler, traceContextBuffer), + threadId); + } + samplingProfiler.profiledThreads.remove(threadId); + try { + callTree.end( + samplingProfiler.callTreePool, samplingProfiler.getInferredSpansMinDurationNs()); + int createdSpans = callTree.spanify(); + if (logger.isDebugEnabled()) { + if (createdSpans > 0) { + logger.debug("Created spans ({}) for thread {}", createdSpans, threadId); + } else { + logger.debug( + "Created no spans for thread {} (count={})", threadId, callTree.getCount()); + } + } + } finally { + callTree.recycle(samplingProfiler.callTreePool, samplingProfiler.rootPool); + } + } + } + + public void serialize(ByteBuffer buf) { + buf.putLong(timestamp); + buf.putShort(getServiceNameIndex()); + buf.putShort(getServiceVersionIndex()); + buf.put(traceContextBuffer); + buf.put(previousContextBuffer); + buf.put(rootContext ? (byte) 1 : (byte) 0); + buf.putLong(threadId); + buf.put(activation ? (byte) 1 : (byte) 0); + } + + public void deserialize(ByteBuffer buf) { + timestamp = buf.getLong(); + serviceName = serviceNameBackMap.get(buf.getShort()); + serviceVersion = serviceVersionBackMap.get(buf.getShort()); + buf.get(traceContextBuffer); + buf.get(previousContextBuffer); + rootContext = buf.get() == 1; + threadId = buf.getLong(); + activation = buf.get() == 1; + } + + private short getServiceNameIndex() { + Short index = serviceNameMap.get(serviceName); + if (index == null) { + index = (short) serviceNameMap.size(); + serviceNameMap.put(serviceName, index); + serviceNameBackMap.put(index, serviceName); + } + return index; + } + + private short getServiceVersionIndex() { + Short index = serviceVersionMap.get(serviceVersion); + if (index == null) { + index = (short) serviceVersionMap.size(); + serviceVersionMap.put(serviceVersion, index); + serviceVersionBackMap.put(index, serviceVersion); + } + return index; + } + } + + /** + * Does not wait but immediately returns the highest sequence which is available for read We never + * want to wait until new elements are available, we just want to process all available events + */ + private static class NoWaitStrategy implements WaitStrategy { + + @Override + public long waitFor( + long sequence, Sequence cursor, Sequence dependentSequence, SequenceBarrier barrier) { + return dependentSequence.get(); + } + + @Override + public void signalAllWhenBlocking() {} + } + + // extracting to a class instead of instantiating an anonymous inner class makes a huge difference + // in allocations + private class WriteActivationEventToFileHandler implements EventPoller.Handler { + @Override + public boolean onEvent(ActivationEvent event, long sequence, boolean endOfBatch) + throws IOException { + if (endOfBatch) { + SamplingProfiler.this.sequence.set(sequence); + } + if (activationEventsFileChannel.size() < MAX_ACTIVATION_EVENTS_FILE_SIZE) { + event.serialize(activationEventsBuffer); + if (!activationEventsBuffer.hasRemaining()) { + flushActivationEvents(); + } + return true; + } + return false; + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SystemNanoClock.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SystemNanoClock.java new file mode 100644 index 000000000..53325cc48 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SystemNanoClock.java @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +public class SystemNanoClock implements NanoClock { + @Override + public long nanoTime() { + return System.nanoTime(); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java new file mode 100644 index 000000000..7341eee5c --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +public class ThreadMatcher { + + private final ThreadGroup systemThreadGroup; + private Thread[] threads = new Thread[16]; + + public ThreadMatcher() { + ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); + while (threadGroup.getParent() != null) { + threadGroup = threadGroup.getParent(); + } + systemThreadGroup = threadGroup; + } + + public void forEachThread( + NonCapturingPredicate predicate, + S1 state1, + NonCapturingConsumer consumer, + S2 state2) { + int count = systemThreadGroup.activeCount(); + do { + int expectedArrayLength = count + (count / 2) + 1; + if (threads.length < expectedArrayLength) { + threads = new Thread[expectedArrayLength]; // slightly grow the array size + } + count = systemThreadGroup.enumerate(threads, true); + // return value of enumerate() must be strictly less than the array size according to javadoc + } while (count >= threads.length); + + for (int i = 0; i < count; i++) { + Thread thread = threads[i]; + if (predicate.test(thread, state1)) { + consumer.accept(thread, state2); + } + threads[i] = null; + } + } + + interface NonCapturingPredicate { + boolean test(T t, S state); + } + + interface NonCapturingConsumer { + void accept(T t, S state); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java new file mode 100644 index 000000000..391548f99 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java @@ -0,0 +1,202 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.asyncprofiler; + +import co.elastic.apm.agent.common.JvmRuntimeInfo; +import co.elastic.apm.agent.common.util.ResourceExtractionUtil; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.annotation.Nullable; + +/** + * Java API for in-process profiling. Serves as a wrapper around async-profiler native library. This + * class is a singleton. The first call to {@link #getInstance(String, int)} initiates loading of + * libasyncProfiler.so. + * + *

This is based on + * https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/java/one/profiler/AsyncProfiler.java, + * under Apache License 2.0. It is modified to allow it to be shaded into the {@code co.elastic.apm} + * namespace + */ +public class AsyncProfiler { + + public static final String SAFEMODE_SYSTEM_PROPERTY_NAME = "AsyncProfiler.safemode"; + + @Nullable private static volatile AsyncProfiler instance; + + private AsyncProfiler() {} + + public static AsyncProfiler getInstance(String profilerLibDirectory, int safemode) { + AsyncProfiler result = AsyncProfiler.instance; + if (result != null) { + return result; + } + synchronized (AsyncProfiler.class) { + if (instance == null) { + if (JvmRuntimeInfo.ofCurrentVM().isJ9VM()) { + throw new IllegalStateException( + "OpenJ9 JVMs are not supported by async profiler. Please set " + + "profiling_inferred_spans_enabled to false"); + } + try { + // set the AsyncProfiler.safemode system property with the configured safemode, so that + // optimizations + // can be applied already at load time. Specifically, if (safemode & 14) == 14 (2, 4 and 8 + // bits are set), then + // async profiler will avoid enabling CompiledMethodLoad events at load time, so to + // workaround a relatd JVM bug + // (https://bugs.openjdk.java.net/browse/JDK-8202883, + // https://bugs.openjdk.java.net/browse/JDK-8173361 and friends). + // safemode can still be set for each profiling session, but it can only be stricter than + // the safemode + // configured at load time. + System.setProperty(SAFEMODE_SYSTEM_PROPERTY_NAME, String.valueOf(safemode)); + loadNativeLibrary(profilerLibDirectory); + } catch (UnsatisfiedLinkError e) { + throw new IllegalStateException( + String.format( + "It is likely that %s is not an executable location. Consider setting " + + "the profiling_inferred_spans_lib_directory property to a directory on a partition that allows execution", + profilerLibDirectory), + e); + } + + instance = new AsyncProfiler(); + } + return instance; + } + } + + static void reset() { + synchronized (AsyncProfiler.class) { + instance = null; + } + } + + private static void loadNativeLibrary(String libraryDirectory) { + String libraryName = getLibraryFileName(); + Path file = + ResourceExtractionUtil.extractResourceToDirectory( + "asyncprofiler/" + libraryName + ".so", + libraryName, + ".so", + Paths.get(libraryDirectory)); + System.load(file.toString()); + } + + static String getLibraryFileName() { + String os = System.getProperty("os.name").toLowerCase(); + String arch = System.getProperty("os.arch").toLowerCase(); + if (os.contains("linux")) { + if (arch.contains("arm") || arch.contains("aarch32")) { + return "libasyncProfiler-linux-arm"; + } else if (arch.contains("aarch")) { + return "libasyncProfiler-linux-aarch64"; + } else if (arch.contains("64")) { + return "libasyncProfiler-linux-x64"; + } else if (arch.contains("86")) { + return "libasyncProfiler-linux-x86"; + } else { + throw new IllegalStateException("Async-profiler does not work on Linux " + arch); + } + } else if (os.contains("mac")) { + if (arch.contains("aarch")) { + throw new IllegalStateException("Async-profiler 1.x does not work on Apple silicon"); + } else { + return "libasyncProfiler-macos-x64"; + } + } else { + throw new IllegalStateException("Async-profiler does not work on " + os); + } + } + + /** + * Stop profiling (without dumping results) + * + * @throws IllegalStateException If profiler is not running + */ + public void stop() throws IllegalStateException { + stop0(); + } + + /** + * Execute an agent-compatible profiling command - the comma-separated list of arguments described + * in arguments.cpp + * + * @param command Profiling command + * @return The command result + * @throws IllegalArgumentException If failed to parse the command + * @throws java.io.IOException If failed to create output file + */ + public String execute(String command) throws IllegalArgumentException, java.io.IOException { + return execute0(command); + } + + /** + * Adds the given thread to the set of profiled threads + * + * @param thread A thread to add; null means current thread + * @throws IllegalStateException If thread has not yet started or has already finished + */ + public void enableProfilingThread(Thread thread) throws IllegalStateException { + filterThread(thread, true); + } + + /** + * Removes the given thread to the set of profiled threads + * + * @param thread A thread to remove; null means current thread + * @throws IllegalStateException If thread has not yet started or has already finished + */ + public void disableProfilingThread(Thread thread) throws IllegalStateException { + filterThread(thread, false); + } + + /** Adds the current thread to the set of profiled threads */ + public void enableProfilingCurrentThread() { + filterThread0(null, true); + } + + /** Removes the current thread to the set of profiled threads */ + public void disableProfilingCurrentThread() throws IllegalStateException { + filterThread0(null, false); + } + + private void filterThread(Thread thread, boolean enable) throws IllegalStateException { + synchronized (thread) { + Thread.State state = thread.getState(); + if (state == Thread.State.NEW || state == Thread.State.TERMINATED) { + return; + } + filterThread0(thread, enable); + } + } + + private native long getSamples(); + + private native void start0(String event, long interval, boolean reset) + throws IllegalStateException; + + private native void stop0() throws IllegalStateException; + + private native String execute0(String command) throws IllegalArgumentException, IOException; + + private native void filterThread0(Thread thread, boolean enable); +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java new file mode 100644 index 000000000..20c866561 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java @@ -0,0 +1,328 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.asyncprofiler; + +import co.elastic.apm.agent.tracer.pooling.Recyclable; +import java.io.File; +import java.io.IOException; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.StandardOpenOption; +import javax.annotation.Nullable; + +/** + * An abstraction similar to {@link MappedByteBuffer} that allows to read the content of a file with + * an API that is similar to {@link ByteBuffer}. + * + *

Instances of this class hold a reusable buffer that contains a subset of the file, or the + * whole file if the buffer's capacity is greater or equal to the file's size. + * + *

Whenever calling a method like {@link #getLong()} or {@link #position(long)} would exceed the + * currently buffered range the same buffer is filled with a different range of the file. + * + *

The downside of {@link MappedByteBuffer} (and the reason for implementing this abstraction) is + * that calling methods like {@link MappedByteBuffer#get()} can increase time-to-safepoint. This is + * because these methods are implemented as JVM intrinsics. When the JVM executes an intrinsic, it + * does not switch to the native execution context which means that it's not ready to enter a + * safepoint whenever a intrinsic runs. As reading a file from disk can get stuck (for example when + * the disk is busy) calling {@link MappedByteBuffer#get()} may take a while to execute. While it's + * executing other threads have to wait for it to finish if the JVM wants to reach a safe point. + */ +class BufferedFile implements Recyclable { + + private static final int SIZE_OF_BYTE = 1; + private static final int SIZE_OF_SHORT = 2; + private static final int SIZE_OF_INT = 4; + private static final int SIZE_OF_LONG = 8; + private ByteBuffer buffer; + private final ByteBuffer bigBuffer; + private final ByteBuffer smallBuffer; + + /** The offset of the file from where the {@link #buffer} starts */ + private long offset; + + private boolean wholeFileInBuffer; + @Nullable private FileChannel fileChannel; + + /** + * @param bigBuffer the buffer to be used to read the whole file if the file fits into it + * @param smallBuffer the buffer to be used to read chunks of the file in case the file is larger + * than bigBuffer. Constantly seeking a file with a large buffer is very bad for performance. + */ + public BufferedFile(ByteBuffer bigBuffer, ByteBuffer smallBuffer) { + this.bigBuffer = bigBuffer; + this.smallBuffer = smallBuffer; + } + + /** + * Sets the file and depending on it's size, may read the file into the {@linkplain #buffer + * buffer} + * + * @param file the file to read from + * @throws IOException If some I/O error occurs + */ + public void setFile(File file) throws IOException { + fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ); + if (fileChannel.size() <= bigBuffer.capacity()) { + buffer = bigBuffer; + read(0, bigBuffer.capacity()); + wholeFileInBuffer = true; + } else { + buffer = smallBuffer; + Buffer buffer = this.buffer; + buffer.flip(); + } + } + + /** + * Returns the position of the file + * + * @return the position of the file + */ + public long position() { + return offset + buffer.position(); + } + + /** + * Skips the provided number of bytes in the file without reading new data. + * + * @param bytesToSkip the number of bytes to skip + */ + public void skip(int bytesToSkip) { + position(position() + bytesToSkip); + } + + /** + * Sets the position of the file without reading new data. + * + * @param pos the new position + */ + public void position(long pos) { + Buffer buffer = this.buffer; + long positionDelta = pos - position(); + long newBufferPos = buffer.position() + positionDelta; + if (0 <= newBufferPos && newBufferPos <= buffer.limit()) { + buffer.position((int) newBufferPos); + } else { + // makes sure that the next ensureRemaining will load from file + buffer.position(0); + buffer.limit(0); + offset = pos; + } + } + + /** + * Ensures that the provided number of bytes are available in the {@linkplain #buffer buffer} + * + * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain + * #buffer buffer} + * @throws IOException If some I/O error occurs + * @throws IllegalStateException If minRemaining is greater than the buffer's capacity + */ + public void ensureRemaining(int minRemaining) throws IOException { + ensureRemaining(minRemaining, buffer.capacity()); + } + + /** + * Ensures that the provided number of bytes are available in the {@linkplain #buffer buffer} + * + * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain + * #buffer buffer} + * @param maxRead the max number of bytes to read from the file in case the buffer does currently + * not hold {@code minRemaining} bytes + * @throws IOException If some I/O error occurs + * @throws IllegalStateException If minRemaining is greater than the buffer's capacity + */ + public void ensureRemaining(int minRemaining, int maxRead) throws IOException { + if (wholeFileInBuffer) { + return; + } + if (minRemaining > buffer.capacity()) { + throw new IllegalStateException( + String.format( + "Length (%d) greater than buffer capacity (%d)", minRemaining, buffer.capacity())); + } + if (buffer.remaining() < minRemaining) { + read(position(), maxRead); + } + } + + /** + * Gets a byte from the current {@linkplain #position() position} of this file. If the {@linkplain + * #buffer buffer} does not fully contain this byte, loads another slice of the file into the + * buffer. + * + * @return The byte at the file's current position + * @throws IOException If some I/O error occurs + */ + public short get() throws IOException { + ensureRemaining(SIZE_OF_BYTE); + return buffer.get(); + } + + /** + * Gets a short from the current {@linkplain #position() position} of this file. If the + * {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file + * into the buffer. + * + * @return The short at the file's current position + * @throws IOException If some I/O error occurs + */ + public short getShort() throws IOException { + ensureRemaining(SIZE_OF_SHORT); + return buffer.getShort(); + } + + /** + * Gets a short from the current {@linkplain #position() position} of this file. If the + * {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file + * into the buffer. + * + * @return The short at the file's current position + * @throws IOException If some I/O error occurs + */ + public int getUnsignedShort() throws IOException { + return getShort() & 0xffff; + } + + /** + * Gets a int from the current {@linkplain #position() position} of this file and converts it to + * an unsigned short. If the {@linkplain #buffer buffer} does not fully contain this int, loads + * another slice of the file into the buffer. + * + * @return The int at the file's current position + * @throws IOException If some I/O error occurs + */ + public int getInt() throws IOException { + ensureRemaining(SIZE_OF_INT); + return buffer.getInt(); + } + + /** + * Gets a long from the current {@linkplain #position() position} of this file. If the {@linkplain + * #buffer buffer} does not fully contain this long, loads another slice of the file into the + * buffer. + * + * @return The long at the file's current position + * @throws IOException If some I/O error occurs + */ + public long getLong() throws IOException { + ensureRemaining(SIZE_OF_LONG); + return buffer.getLong(); + } + + /** + * Gets a byte from the underlying buffer without checking if this part of the file is actually in + * the buffer. + * + *

Always mare sure to call {@link #ensureRemaining} before. + * + * @return The byte at the file's current position + * @throws java.nio.BufferUnderflowException If the buffer's current position is not smaller than + * its limit + */ + public byte getUnsafe() { + return buffer.get(); + } + + /** + * Gets a short from the underlying buffer without checking if this part of the file is actually + * in the buffer. + * + *

Always mare sure to call {@link #ensureRemaining} before. + * + * @return The byte at the file's current position + * @throws java.nio.BufferUnderflowException If there are fewer than two bytes remaining in this + * buffer + */ + public short getUnsafeShort() { + return buffer.getShort(); + } + + /** + * Gets an int from the underlying buffer without checking if this part of the file is actually in + * the buffer. + * + *

Always mare sure to call {@link #ensureRemaining} before. + * + * @return The byte at the file's current position + * @throws java.nio.BufferUnderflowException If there are fewer than four bytes remaining in this + * buffer + */ + public int getUnsafeInt() { + return buffer.getInt(); + } + + /** + * Gets a long from the underlying buffer without checking if this part of the file is actually in + * the buffer. + * + *

Always mare sure to call {@link #ensureRemaining} before. + * + * @return The byte at the file's current position + * @throws java.nio.BufferUnderflowException If there are fewer than eight bytes remaining in this + * buffer + */ + public long getUnsafeLong() { + return buffer.getLong(); + } + + public long size() throws IOException { + if (fileChannel == null) { + throw new IllegalStateException("setFile has not been called yet"); + } + return fileChannel.size(); + } + + public boolean isSet() { + return fileChannel != null; + } + + @Override + public void resetState() { + if (fileChannel == null) { + throw new IllegalStateException("setFile has not been called yet"); + } + Buffer buffer = this.buffer; + buffer.clear(); + offset = 0; + wholeFileInBuffer = false; + try { + fileChannel.close(); + } catch (IOException ignore) { + } + fileChannel = null; + this.buffer = null; + } + + private void read(long offset, int limit) throws IOException { + if (limit > buffer.capacity()) { + limit = buffer.capacity(); + } + Buffer buffer = this.buffer; + buffer.clear(); + fileChannel.position(offset); + buffer.limit(limit); + fileChannel.read(this.buffer); + buffer.flip(); + this.offset = offset; + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java new file mode 100644 index 000000000..34023c905 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java @@ -0,0 +1,484 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.asyncprofiler; + +import co.elastic.apm.agent.common.util.WildcardMatcher; +import co.elastic.apm.agent.impl.transaction.StackFrame; +import co.elastic.apm.agent.profiler.collections.Int2IntHashMap; +import co.elastic.apm.agent.profiler.collections.Int2ObjectHashMap; +import co.elastic.apm.agent.profiler.collections.Long2LongHashMap; +import co.elastic.apm.agent.profiler.collections.Long2ObjectHashMap; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import co.elastic.apm.agent.tracer.pooling.Recyclable; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import javax.annotation.Nullable; + +/** + * Parses the binary JFR file created by async-profiler. May not work with JFR files created by an + * actual flight recorder. + * + *

The implementation is tuned with to minimize allocations when parsing a JFR file. Most data + * structures can be reused by first {@linkplain #resetState() resetting the state} and then + * {@linkplain #parse(File, List, List) parsing} another file. + */ +public class JfrParser implements Recyclable { + + private static final Logger logger = LoggerFactory.getLogger(JfrParser.class); + + private static final byte[] MAGIC_BYTES = new byte[] {'F', 'L', 'R', '\0'}; + private static final Set JAVA_FRAME_TYPES = + new HashSet<>(Arrays.asList("Interpreted", "JIT compiled", "Inlined")); + private static final int BIG_FILE_BUFFER_SIZE = 5 * 1024 * 1024; + private static final int SMALL_FILE_BUFFER_SIZE = 4 * 1024; + private static final String SYMBOL_EXCLUDED = "3x cluded"; + private static final String SYMBOL_NULL = "n u11"; + private static final StackFrame FRAME_EXCLUDED = new StackFrame("excluded", "excluded"); + private static final StackFrame FRAME_NULL = new StackFrame("null", "null"); + + private final BufferedFile bufferedFile; + private final Int2IntHashMap classIdToClassNameSymbolId = new Int2IntHashMap(-1); + private final Int2IntHashMap symbolIdToPos = new Int2IntHashMap(-1); + private final Int2ObjectHashMap symbolIdToString = new Int2ObjectHashMap(); + private final Int2IntHashMap stackTraceIdToFilePositions = new Int2IntHashMap(-1); + private final Long2LongHashMap nativeTidToJavaTid = new Long2LongHashMap(-1); + private final Long2ObjectHashMap frameIdToFrame = + new Long2ObjectHashMap(); + private final Long2LongHashMap frameIdToMethodSymbol = new Long2LongHashMap(-1); + private final Long2LongHashMap frameIdToClassId = new Long2LongHashMap(-1); + // used to resolve a symbol with minimal allocations + private final StringBuilder symbolBuilder = new StringBuilder(); + private long eventsOffset; + private long metadataOffset; + @Nullable private boolean[] isJavaFrameType; + @Nullable private List excludedClasses; + @Nullable private List includedClasses; + + public JfrParser() { + this( + ByteBuffer.allocateDirect(BIG_FILE_BUFFER_SIZE), + ByteBuffer.allocateDirect(SMALL_FILE_BUFFER_SIZE)); + } + + JfrParser(ByteBuffer bigBuffer, ByteBuffer smallBuffer) { + bufferedFile = new BufferedFile(bigBuffer, smallBuffer); + } + + /** + * Initializes the parser to make it ready for {@link #resolveStackTrace(long, boolean, List, + * int)} to be called. + * + * @param file the JFR file to parse + * @param excludedClasses Class names to exclude in stack traces (has an effect on {@link + * #resolveStackTrace(long, boolean, List, int)}) + * @param includedClasses Class names to include in stack traces (has an effect on {@link + * #resolveStackTrace(long, boolean, List, int)}) + * @throws IOException if some I/O error occurs + */ + public void parse( + File file, List excludedClasses, List includedClasses) + throws IOException { + this.excludedClasses = excludedClasses; + this.includedClasses = includedClasses; + bufferedFile.setFile(file); + long fileSize = bufferedFile.size(); + if (fileSize < 16) { + throw new IllegalStateException( + "Unexpected sampling profiler error, everything else should work as expected. " + + "Please report to us with as many details, including OS and JVM details."); + } + logger.debug("Parsing {} ({} bytes)", file, fileSize); + bufferedFile.ensureRemaining(16, 16); + for (byte magicByte : MAGIC_BYTES) { + if (bufferedFile.get() != magicByte) { + throw new IllegalArgumentException("Not a JFR file"); + } + } + short major = bufferedFile.getShort(); + short minor = bufferedFile.getShort(); + if (major != 0 || minor != 9) { + throw new IllegalArgumentException( + String.format("Can only parse version 0.9. Was %d.%d", major, minor)); + } + metadataOffset = bufferedFile.getLong(); + eventsOffset = bufferedFile.position(); + + long checkpointOffset = parseMetadata(metadataOffset); + parseCheckpoint(checkpointOffset); + } + + private long parseMetadata(long metadataOffset) throws IOException { + bufferedFile.position(metadataOffset); + bufferedFile.ensureRemaining(8, 8); + int size = bufferedFile.getInt(); + expectEventType(EventTypeId.EVENT_METADATA); + bufferedFile.skip(size - 16); + bufferedFile.ensureRemaining(8, 8); + return bufferedFile.getLong(); + } + + private void expectEventType(int expectedEventType) throws IOException { + int eventType = bufferedFile.getInt(); + if (eventType != expectedEventType) { + throw new IOException("Expected " + expectedEventType + " but got " + eventType); + } + } + + private void parseCheckpoint(long checkpointOffset) throws IOException { + bufferedFile.position(checkpointOffset); + int size = bufferedFile.getInt(); // size + expectEventType(EventTypeId.EVENT_CHECKPOINT); + bufferedFile.getLong(); // stop timestamp + bufferedFile.getLong(); // previous checkpoint - always 0 in async-profiler + while (bufferedFile.position() < metadataOffset) { + parseContent(); + } + } + + private void parseContent() throws IOException { + BufferedFile bufferedFile = this.bufferedFile; + int contentTypeId = bufferedFile.getInt(); + logger.debug("Parsing content type {}", contentTypeId); + int count = bufferedFile.getInt(); + switch (contentTypeId) { + case ContentTypeId.CONTENT_THREAD: + for (int i = 0; i < count; i++) { + int threadId = bufferedFile.getInt(); + String threadName = readUtf8String().toString(); + } + break; + case ContentTypeId.CONTENT_JAVA_THREAD: + for (int i = 0; i < count; i++) { + bufferedFile.ensureRemaining(16); + long javaThreadId = bufferedFile.getUnsafeLong(); + int nativeThreadId = bufferedFile.getUnsafeInt(); + int threadGroup = bufferedFile.getUnsafeInt(); + nativeTidToJavaTid.put(nativeThreadId, javaThreadId); + } + break; + case ContentTypeId.CONTENT_THREAD_GROUP: + // no info + break; + case ContentTypeId.CONTENT_STACKTRACE: + for (int i = 0; i < count; i++) { + bufferedFile.ensureRemaining(13); + int pos = (int) bufferedFile.position(); + // always an integer + // see profiler.h + // MAX_CALLTRACES = 65536 + int stackTraceKey = (int) bufferedFile.getUnsafeLong(); + this.stackTraceIdToFilePositions.put(stackTraceKey, pos); + bufferedFile.getUnsafe(); // truncated + int numFrames = bufferedFile.getUnsafeInt(); + int sizeOfFrame = 13; + bufferedFile.skip(numFrames * sizeOfFrame); + } + break; + case ContentTypeId.CONTENT_CLASS: + for (int i = 0; i < count; i++) { + bufferedFile.ensureRemaining(26); + // classId is an incrementing integer, no way there are more than 2 billion distinct ones + int classId = (int) bufferedFile.getUnsafeLong(); + bufferedFile.getUnsafeLong(); // loader class + // symbol ids are incrementing integers, no way there are more than 2 billion distinct + // ones + int classNameSymbolId = (int) bufferedFile.getUnsafeLong(); + classIdToClassNameSymbolId.put(classId, classNameSymbolId); // class name + bufferedFile.getUnsafeShort(); // access flags + } + break; + case ContentTypeId.CONTENT_METHOD: + for (int i = 1; i <= count; i++) { + bufferedFile.ensureRemaining(35); + long id = bufferedFile.getUnsafeLong(); + // classId is an incrementing integer, no way there are more than 2 billion distinct ones + int classId = (int) bufferedFile.getUnsafeLong(); + // symbol ids are incrementing integers, no way there are more than 2 billion distinct + // ones + int methodNameSymbolId = (int) bufferedFile.getUnsafeLong(); + frameIdToFrame.put(id, FRAME_NULL); + frameIdToClassId.put(id, classId); + frameIdToMethodSymbol.put(id, methodNameSymbolId); + bufferedFile.getUnsafeLong(); // signature + bufferedFile.getUnsafeShort(); // modifiers + bufferedFile.getUnsafe(); // hidden + } + break; + case ContentTypeId.CONTENT_SYMBOL: + for (int i = 0; i < count; i++) { + // symbol ids are incrementing integers, no way there are more than 2 billion distinct + // ones + int symbolId = (int) bufferedFile.getLong(); + int pos = (int) bufferedFile.position(); + symbolIdToPos.put(symbolId, pos); + symbolIdToString.put(symbolId, SYMBOL_NULL); + skipString(); + } + break; + case ContentTypeId.CONTENT_STATE: + // we're not really interested in the thread states + // but we sill have to consume the bytes + for (int i = 1; i <= count; i++) { + bufferedFile.getShort(); + skipString(); + } + break; + case ContentTypeId.CONTENT_FRAME_TYPE: + isJavaFrameType = new boolean[count + 1]; + for (int i = 1; i <= count; i++) { + int id = bufferedFile.get(); + if (i != id) { + throw new IllegalStateException("Expecting ids to be incrementing"); + } + isJavaFrameType[id] = JAVA_FRAME_TYPES.contains(readUtf8String().toString()); + } + break; + default: + throw new IOException("Unknown content type " + contentTypeId); + } + } + + private void skipString() throws IOException { + int stringLength = bufferedFile.getUnsignedShort(); + bufferedFile.skip(stringLength); + } + + /** + * Invokes the callback for each stack trace event in the JFR file. + * + * @param callback called for each stack trace event + * @throws IOException if some I/O error occurs + */ + public void consumeStackTraces(StackTraceConsumer callback) throws IOException { + if (!bufferedFile.isSet()) { + throw new IllegalStateException("consumeStackTraces was called before parse"); + } + bufferedFile.position(eventsOffset); + while (bufferedFile.position() < metadataOffset) { + bufferedFile.ensureRemaining(30); + int size = bufferedFile.getUnsafeInt(); + int eventType = bufferedFile.getUnsafeInt(); + if (eventType == EventTypeId.EVENT_RECORDING) { + return; + } + if (eventType != EventTypeId.EVENT_EXECUTION_SAMPLE) { + throw new IOException( + "Expected " + EventTypeId.EVENT_EXECUTION_SAMPLE + " but got " + eventType); + } + long nanoTime = bufferedFile.getUnsafeLong(); + int tid = bufferedFile.getUnsafeInt(); + long stackTraceId = bufferedFile.getUnsafeLong(); + short threadState = bufferedFile.getUnsafeShort(); + long javaThreadId = nativeTidToJavaTid.get(tid); + if (javaThreadId != -1) { + callback.onCallTree(javaThreadId, stackTraceId, nanoTime); + } + } + } + + /** + * Resolves the stack trace with the given {@code stackTraceId}. + * + *

Note that his allocates strings for symbols in case a stack frame has not already been + * resolved for the current JFR file yet. These strings are currently not cached so this can + * create some GC pressure. + * + *

Excludes frames based on the {@link WildcardMatcher}s supplied to {@link #parse(File, List, + * List)}. + * + * @param stackTraceId The id of the stack traced. Used to look up the position of the file in + * which the given stack trace is stored via {@link #stackTraceIdToFilePositions}. + * @param onlyJavaFrames If {@code true}, will only resolve {@code Interpreted}, {@code JIT + * compiled} and {@code Inlined} frames. If {@code false}, will also resolve {@code Native}, + * {@code Kernel} and {@code C++} frames. + * @param stackFrames The mutable list where the stack frames are written to. Don't forget to + * {@link List#clear()} the list before calling this method if the list is reused. + * @param maxStackDepth The max size of the stackFrames list (excluded frames don't take up + * space). In contrast to async-profiler's {@code jstackdepth} argument this does not truncate + * the bottom of the stack, only the top. This is important to properly create a call tree + * without making it overly complex. + * @throws IOException if there is an error reading in current buffer + */ + public void resolveStackTrace( + long stackTraceId, boolean onlyJavaFrames, List stackFrames, int maxStackDepth) + throws IOException { + if (!bufferedFile.isSet()) { + throw new IllegalStateException("getStackTrace was called before parse"); + } + long position = bufferedFile.position(); + bufferedFile.position(stackTraceIdToFilePositions.get((int) stackTraceId)); + bufferedFile.ensureRemaining(13); + long stackTraceIdFromFile = bufferedFile.getUnsafeLong(); + assert stackTraceId == stackTraceIdFromFile; + bufferedFile.getUnsafe(); // truncated + int numFrames = bufferedFile.getUnsafeInt(); + for (int i = 0; i < numFrames; i++) { + bufferedFile.ensureRemaining(13); + long frameId = bufferedFile.getUnsafeLong(); + bufferedFile.getUnsafeInt(); // bci (always set to 0 by async-profiler) + byte frameType = bufferedFile.getUnsafe(); + addFrameIfIncluded(stackFrames, onlyJavaFrames, frameId, frameType); + if (stackFrames.size() > maxStackDepth) { + stackFrames.remove(0); + } + } + bufferedFile.position(position); + } + + private void addFrameIfIncluded( + List stackFrames, boolean onlyJavaFrames, long frameId, byte frameType) + throws IOException { + if (!onlyJavaFrames || isJavaFrameType(frameType)) { + StackFrame stackFrame = resolveStackFrame(frameId); + if (stackFrame != FRAME_EXCLUDED) { + stackFrames.add(stackFrame); + } + } + } + + private boolean isJavaFrameType(byte frameType) { + return isJavaFrameType[frameType]; + } + + private String resolveSymbol(int id, boolean classSymbol) throws IOException { + String symbol = symbolIdToString.get(id); + if (symbol != SYMBOL_NULL) { + return symbol; + } + StringBuilder symbolBuilder = resolveSymbolBuilder(symbolIdToPos.get(id), classSymbol); + if (classSymbol && !isClassIncluded(symbolBuilder)) { + symbol = SYMBOL_EXCLUDED; + } else { + symbol = symbolBuilder.toString(); + } + symbolIdToString.put(id, symbol); + return symbol; + } + + private StringBuilder resolveSymbolBuilder(int pos, boolean replaceSlashWithDot) + throws IOException { + long currentPos = bufferedFile.position(); + bufferedFile.position(pos); + try { + return readUtf8String(replaceSlashWithDot); + } finally { + bufferedFile.position(currentPos); + } + } + + private boolean isClassIncluded(CharSequence className) { + return WildcardMatcher.isAnyMatch(includedClasses, className) + && WildcardMatcher.isNoneMatch(excludedClasses, className); + } + + private StackFrame resolveStackFrame(long frameId) throws IOException { + StackFrame stackFrame = frameIdToFrame.get(frameId); + if (stackFrame != FRAME_NULL) { + return stackFrame; + } + String className = + resolveSymbol(classIdToClassNameSymbolId.get((int) frameIdToClassId.get(frameId)), true); + if (className == SYMBOL_EXCLUDED) { + stackFrame = FRAME_EXCLUDED; + } else { + String method = resolveSymbol((int) frameIdToMethodSymbol.get(frameId), false); + stackFrame = new StackFrame(className, Objects.requireNonNull(method)); + } + frameIdToFrame.put(frameId, stackFrame); + return stackFrame; + } + + private StringBuilder readUtf8String() throws IOException { + return readUtf8String(false); + } + + private StringBuilder readUtf8String(boolean replaceSlashWithDot) throws IOException { + int size = bufferedFile.getUnsignedShort(); + bufferedFile.ensureRemaining(size); + StringBuilder symbolBuilder = this.symbolBuilder; + symbolBuilder.setLength(0); + for (int i = 0; i < size; i++) { + char c = (char) bufferedFile.getUnsafe(); + if (replaceSlashWithDot && c == '/') { + symbolBuilder.append('.'); + } else { + symbolBuilder.append(c); + } + } + return symbolBuilder; + } + + @Override + public void resetState() { + bufferedFile.resetState(); + eventsOffset = 0; + metadataOffset = 0; + isJavaFrameType = null; + classIdToClassNameSymbolId.clear(); + stackTraceIdToFilePositions.clear(); + frameIdToFrame.clear(); + frameIdToMethodSymbol.clear(); + frameIdToClassId.clear(); + symbolBuilder.setLength(0); + excludedClasses = null; + includedClasses = null; + symbolIdToPos.clear(); + symbolIdToString.clear(); + } + + public interface StackTraceConsumer { + + /** + * @param threadId The {@linkplain Thread#getId() Java thread id} for with the event was + * recorded. + * @param stackTraceId The id of the stack trace event. Can be used to resolve the stack trace + * via {@link #resolveStackTrace(long, boolean, List, int)} + * @param nanoTime The timestamp of the event which can be correlated with {@link + * System#nanoTime()} + * @throws IOException if there is any error reading stack trace + */ + void onCallTree(long threadId, long stackTraceId, long nanoTime) throws IOException; + } + + private interface EventTypeId { + int EVENT_METADATA = 0; + int EVENT_CHECKPOINT = 1; + int EVENT_RECORDING = 10; + int EVENT_EXECUTION_SAMPLE = 20; + } + + private interface ContentTypeId { + int CONTENT_THREAD = 7; + int CONTENT_JAVA_THREAD = 8; + int CONTENT_STACKTRACE = 9; + int CONTENT_CLASS = 10; + int CONTENT_THREAD_GROUP = 31; + int CONTENT_METHOD = 32; + int CONTENT_SYMBOL = 33; + int CONTENT_STATE = 34; + int CONTENT_FRAME_TYPE = 47; + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/package-info.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/package-info.java new file mode 100644 index 000000000..776f677cb --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +@NonnullApi +package co.elastic.apm.agent.profiler.asyncprofiler; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java new file mode 100644 index 000000000..ef04c0b27 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +/** Utility functions for collection objects. */ +public class CollectionUtil { + /** + * Validate that a load factor is in the range of 0.1 to 0.9. + * + *

Load factors in the range 0.5 - 0.7 are recommended for open-addressing with linear probing. + * + * @param loadFactor to be validated. + */ + public static void validateLoadFactor(final float loadFactor) { + if (loadFactor < 0.1f || loadFactor > 0.9f) { + throw new IllegalArgumentException( + "load factor must be in the range of 0.1 to 0.9: " + loadFactor); + } + } + + /** + * Fast method of finding the next power of 2 greater than or equal to the supplied value. + * + *

If the value is <= 0 then 1 will be returned. + * + *

This method is not suitable for {@link Integer#MIN_VALUE} or numbers greater than 2^30. When + * provided then {@link Integer#MIN_VALUE} will be returned. + * + * @param value from which to search for next power of 2. + * @return The next power of 2 or the value itself if it is a power of 2. + */ + public static int findNextPositivePowerOfTwo(final int value) { + return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(value - 1)); + } + + /** + * Fast method of finding the next power of 2 greater than or equal to the supplied value. + * + *

If the value is <= 0 then 1 will be returned. + * + *

This method is not suitable for {@link Long#MIN_VALUE} or numbers greater than 2^62. When + * provided then {@link Long#MIN_VALUE} will be returned. + * + * @param value from which to search for next power of 2. + * @return The next power of 2 or the value itself if it is a power of 2. + */ + public static long findNextPositivePowerOfTwo(final long value) { + return 1L << (Long.SIZE - Long.numberOfLeadingZeros(value - 1)); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java new file mode 100644 index 000000000..8c59e567b --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java @@ -0,0 +1,131 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +/** Hashing functions for applying to integers. */ +public class Hashing { + /** Default load factor to be used in open addressing hashed data structures. */ + public static final float DEFAULT_LOAD_FACTOR = 0.55f; + + /** + * Generate a hash for an int value. This is a no op. + * + * @param value to be hashed. + * @return the hashed value. + */ + public static int hash(final int value) { + return value * 31; + } + + /** + * Generate a hash for an long value. + * + * @param value to be hashed. + * @return the hashed value. + */ + public static int hash(final long value) { + long hash = value * 31; + hash = (int) hash ^ (int) (hash >>> 32); + + return (int) hash; + } + + /** + * Generate a hash for a int value. + * + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value. + */ + public static int hash(final int value, final int mask) { + final int hash = value * 31; + + return hash & mask; + } + + /** + * Generate a hash for a K value. + * + * @param is the type of value + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value. + */ + public static int hash(final K value, final int mask) { + final int hash = value.hashCode(); + + return hash & mask; + } + + /** + * Generate a hash for a long value. + * + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value. + */ + public static int hash(final long value, final int mask) { + long hash = value * 31; + hash = (int) hash ^ (int) (hash >>> 32); + + return (int) hash & mask; + } + + /** + * Generate an even hash for a int value. + * + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value which is always even. + */ + public static int evenHash(final int value, final int mask) { + final int hash = (value << 1) - (value << 8); + + return hash & mask; + } + + /** + * Generate an even hash for a long value. + * + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value which is always even. + */ + public static int evenHash(final long value, final int mask) { + int hash = (int) value ^ (int) (value >>> 32); + hash = (hash << 1) - (hash << 8); + + return hash & mask; + } + + /** + * Combined two 32 bit keys into a 64-bit compound. + * + * @param keyPartA to make the upper bits + * @param keyPartB to make the lower bits. + * @return the compound key + */ + public static long compoundKey(final int keyPartA, final int keyPartB) { + return ((long) keyPartA << 32) | (keyPartB & 0xFFFF_FFFFL); + } + + public static int hashCode(long value) { + return (int) (value ^ (value >>> 32)); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java new file mode 100644 index 000000000..2e827529b --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java @@ -0,0 +1,863 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** A open addressing with linear probing hash map specialised for primitive key and value pairs. */ +public class Int2IntHashMap implements Map, Serializable { + static final int MIN_CAPACITY = 8; + + private final float loadFactor; + private final int missingValue; + private int resizeThreshold; + private int size = 0; + private final boolean shouldAvoidAllocation; + + private int[] entries; + private KeySet keySet; + private ValueCollection values; + private EntrySet entrySet; + + public Int2IntHashMap(final int missingValue) { + this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, missingValue); + } + + public Int2IntHashMap(final int initialCapacity, final float loadFactor, final int missingValue) { + this(initialCapacity, loadFactor, missingValue, true); + } + + /** + * @param initialCapacity for the map to override {@link #MIN_CAPACITY} + * @param loadFactor for the map to override {@link Hashing#DEFAULT_LOAD_FACTOR}. + * @param missingValue for the map that represents null. + * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. + */ + public Int2IntHashMap( + final int initialCapacity, + final float loadFactor, + final int missingValue, + final boolean shouldAvoidAllocation) { + validateLoadFactor(loadFactor); + + this.loadFactor = loadFactor; + this.missingValue = missingValue; + this.shouldAvoidAllocation = shouldAvoidAllocation; + + capacity(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity))); + } + + /** + * The value to be used as a null marker in the map. + * + * @return value to be used as a null marker in the map. + */ + public int missingValue() { + return missingValue; + } + + /** + * Get the load factor applied for resize operations. + * + * @return the load factor applied for resize operations. + */ + public float loadFactor() { + return loadFactor; + } + + /** + * Get the total capacity for the map to which the load factor will be a fraction of. + * + * @return the total capacity for the map. + */ + public int capacity() { + return entries.length >> 2; + } + + /** + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. + * + * @return the threshold when the map will resize. + */ + public int resizeThreshold() { + return resizeThreshold; + } + + /** {@inheritDoc} */ + public int size() { + return size; + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return size == 0; + } + + public int get(final int key) { + final int[] entries = this.entries; + final int missingValue = this.missingValue; + final int mask = entries.length - 1; + int index = Hashing.evenHash(key, mask); + + int value = missingValue; + while (entries[index + 1] != missingValue) { + if (entries[index] == key) { + value = entries[index + 1]; + break; + } + + index = next(index, mask); + } + + return value; + } + + /** + * Put a key value pair in the map. + * + * @param key lookup key + * @param value new value, must not be initialValue + * @return current counter value associated with key, or initialValue if none found + * @throws IllegalArgumentException if value is missingValue + */ + public int put(final int key, final int value) { + if (value == missingValue) { + throw new IllegalArgumentException("cannot accept missingValue"); + } + + final int[] entries = this.entries; + final int missingValue = this.missingValue; + final int mask = entries.length - 1; + int index = Hashing.evenHash(key, mask); + int oldValue = missingValue; + + while (entries[index + 1] != missingValue) { + if (entries[index] == key) { + oldValue = entries[index + 1]; + break; + } + + index = next(index, mask); + } + + if (oldValue == missingValue) { + ++size; + entries[index] = key; + } + + entries[index + 1] = value; + + increaseCapacity(); + + return oldValue; + } + + private void increaseCapacity() { + if (size > resizeThreshold) { + // entries.length = 2 * capacity + final int newCapacity = entries.length; + rehash(newCapacity); + } + } + + private void rehash(final int newCapacity) { + final int[] oldEntries = entries; + final int missingValue = this.missingValue; + final int length = entries.length; + + capacity(newCapacity); + + final int[] newEntries = entries; + final int mask = entries.length - 1; + + for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { + final int value = oldEntries[keyIndex + 1]; + if (value != missingValue) { + final int key = oldEntries[keyIndex]; + int index = Hashing.evenHash(key, mask); + + while (newEntries[index + 1] != missingValue) { + index = next(index, mask); + } + + newEntries[index] = key; + newEntries[index + 1] = value; + } + } + } + + /** + * Primitive specialised forEach implementation. + * + *

NB: Renamed from forEach to avoid overloading on parameter types of lambda expression, which + * doesn't play well with type inference in lambda expressions. + * + * @param consumer a callback called for each key/value pair in the map. + */ + public void intForEach(final IntIntConsumer consumer) { + final int[] entries = this.entries; + final int missingValue = this.missingValue; + final int length = entries.length; + + for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { + if (entries[keyIndex + 1] != missingValue) // lgtm [java/index-out-of-bounds] + { + consumer.accept( + entries[keyIndex], entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] + } + } + } + + /** + * Int primitive specialised containsKey. + * + * @param key the key to check. + * @return true if the map contains key as a key, false otherwise. + */ + public boolean containsKey(final int key) { + return get(key) != missingValue; + } + + /** + * Does the map contain the value. + * + * @param value to be tested against contained values. + * @return true if contained otherwise value. + */ + public boolean containsValue(final int value) { + boolean found = false; + if (value != missingValue) { + final int[] entries = this.entries; + final int length = entries.length; + + for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { + if (value == entries[valueIndex]) { + found = true; + break; + } + } + } + + return found; + } + + /** {@inheritDoc} */ + public void clear() { + if (size > 0) { + Arrays.fill(entries, missingValue); + size = 0; + } + } + + /** + * Compact the backing arrays by rehashing with a capacity just larger than current size and + * giving consideration to the load factor. + */ + public void compact() { + final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); + rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); + } + + // ---------------- Boxed Versions Below ---------------- + + /** {@inheritDoc} */ + public Integer get(final Object key) { + return valOrNull(get((int) key)); + } + + /** {@inheritDoc} */ + public Integer put(final Integer key, final Integer value) { + return valOrNull(put((int) key, (int) value)); + } + + /** {@inheritDoc} */ + public boolean containsKey(final Object key) { + return containsKey((int) key); + } + + /** {@inheritDoc} */ + public boolean containsValue(final Object value) { + return containsValue((int) value); + } + + /** {@inheritDoc} */ + public void putAll(final Map map) { + for (final Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** {@inheritDoc} */ + public KeySet keySet() { + if (null == keySet) { + keySet = new KeySet(); + } + + return keySet; + } + + /** {@inheritDoc} */ + public ValueCollection values() { + if (null == values) { + values = new ValueCollection(); + } + + return values; + } + + /** {@inheritDoc} */ + public EntrySet entrySet() { + if (null == entrySet) { + entrySet = new EntrySet(); + } + + return entrySet; + } + + /** {@inheritDoc} */ + public Integer remove(final Object key) { + return valOrNull(remove((int) key)); + } + + public int remove(final int key) { + final int[] entries = this.entries; + final int missingValue = this.missingValue; + final int mask = entries.length - 1; + int keyIndex = Hashing.evenHash(key, mask); + + int oldValue = missingValue; + while (entries[keyIndex + 1] != missingValue) { + if (entries[keyIndex] == key) { + oldValue = entries[keyIndex + 1]; + entries[keyIndex + 1] = missingValue; + size--; + + compactChain(keyIndex); + + break; + } + + keyIndex = next(keyIndex, mask); + } + + return oldValue; + } + + @SuppressWarnings("FinalParameters") + private void compactChain(int deleteKeyIndex) { + final int[] entries = this.entries; + final int missingValue = this.missingValue; + final int mask = entries.length - 1; + int keyIndex = deleteKeyIndex; + + while (true) { + keyIndex = next(keyIndex, mask); + if (entries[keyIndex + 1] == missingValue) { + break; + } + + final int hash = Hashing.evenHash(entries[keyIndex], mask); + + if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) + || (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { + entries[deleteKeyIndex] = entries[keyIndex]; + entries[deleteKeyIndex + 1] = entries[keyIndex + 1]; + + entries[keyIndex + 1] = missingValue; + deleteKeyIndex = keyIndex; + } + } + } + + /** + * Get the minimum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} + * + * @return the minimum value stored in the map. + */ + public int minValue() { + final int missingValue = this.missingValue; + int min = size == 0 ? missingValue : Integer.MAX_VALUE; + + final int[] entries = this.entries; + final int length = entries.length; + + for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { + final int value = entries[valueIndex]; + if (value != missingValue) { + min = Math.min(min, value); + } + } + + return min; + } + + /** + * Get the maximum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} + * + * @return the maximum value stored in the map. + */ + public int maxValue() { + final int missingValue = this.missingValue; + int max = size == 0 ? missingValue : Integer.MIN_VALUE; + + final int[] entries = this.entries; + final int length = entries.length; + + for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { + final int value = entries[valueIndex]; + if (value != missingValue) { + max = Math.max(max, value); + } + } + + return max; + } + + /** {@inheritDoc} */ + public String toString() { + if (isEmpty()) { + return "{}"; + } + + final EntryIterator entryIterator = new EntryIterator(); + entryIterator.reset(); + + final StringBuilder sb = new StringBuilder().append('{'); + while (true) { + entryIterator.next(); + sb.append(entryIterator.getIntKey()).append('=').append(entryIterator.getIntValue()); + if (!entryIterator.hasNext()) { + return sb.append('}').toString(); + } + sb.append(',').append(' '); + } + } + + /** + * Primitive specialised version of {@link #replace(Object, Object)} + * + * @param key key with which the specified value is associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or {@link #missingValue()} if + * there was no mapping for the key. + */ + public int replace(final int key, final int value) { + int curValue = get(key); + if (curValue != missingValue) { + curValue = put(key, value); + } + + return curValue; + } + + /** + * Primitive specialised version of {@link #replace(Object, Object, Object)} + * + * @param key key with which the specified value is associated + * @param oldValue value expected to be associated with the specified key + * @param newValue value to be associated with the specified key + * @return {@code true} if the value was replaced + */ + public boolean replace(final int key, final int oldValue, final int newValue) { + final int curValue = get(key); + if (curValue != oldValue || curValue == missingValue) { + return false; + } + + put(key, newValue); + + return true; + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Map)) { + return false; + } + + final Map that = (Map) o; + + return size == that.size() && entrySet().equals(that.entrySet()); + } + + public int hashCode() { + return entrySet().hashCode(); + } + + private static int next(final int index, final int mask) { + return (index + 2) & mask; + } + + private void capacity(final int newCapacity) { + final int entriesLength = newCapacity * 2; + if (entriesLength < 0) { + throw new IllegalStateException("max capacity reached at size=" + size); + } + + /*@DoNotSub*/ resizeThreshold = (int) (newCapacity * loadFactor); + entries = new int[entriesLength]; + Arrays.fill(entries, missingValue); + } + + private Integer valOrNull(final int value) { + return value == missingValue ? null : value; + } + + // ---------------- Utility Classes ---------------- + + abstract class AbstractIterator implements Serializable { + protected boolean isPositionValid = false; + private int remaining; + private int positionCounter; + private int stopCounter; + + final void reset() { + isPositionValid = false; + remaining = Int2IntHashMap.this.size; + final int missingValue = Int2IntHashMap.this.missingValue; + final int[] entries = Int2IntHashMap.this.entries; + final int capacity = entries.length; + + int keyIndex = capacity; + if (entries[capacity - 1] != missingValue) { + keyIndex = 0; + for (; keyIndex < capacity; keyIndex += 2) { + if (entries[keyIndex + 1] == missingValue) // lgtm [java/index-out-of-bounds] + { + break; + } + } + } + + stopCounter = keyIndex; + positionCounter = keyIndex + capacity; + } + + protected final int keyPosition() { + return positionCounter & entries.length - 1; + } + + public int remaining() { + return remaining; + } + + public boolean hasNext() { + return remaining > 0; + } + + protected final void findNext() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + final int[] entries = Int2IntHashMap.this.entries; + final int missingValue = Int2IntHashMap.this.missingValue; + final int mask = entries.length - 1; + + for (int keyIndex = positionCounter - 2; keyIndex >= stopCounter; keyIndex -= 2) { + final int index = keyIndex & mask; + if (entries[index + 1] != missingValue) { + isPositionValid = true; + positionCounter = keyIndex; + --remaining; + return; + } + } + + isPositionValid = false; + throw new IllegalStateException(); + } + + public void remove() { + if (isPositionValid) { + final int position = keyPosition(); + entries[position + 1] = missingValue; + --size; + + compactChain(position); + + isPositionValid = false; + } else { + throw new IllegalStateException(); + } + } + } + + /** Iterator over keys which supports access to unboxed keys. */ + public final class KeyIterator extends AbstractIterator implements Iterator { + public Integer next() { + return nextValue(); + } + + public int nextValue() { + findNext(); + + return entries[keyPosition()]; + } + } + + /** Iterator over values which supports access to unboxed values. */ + public final class ValueIterator extends AbstractIterator implements Iterator { + public Integer next() { + return nextValue(); + } + + public int nextValue() { + findNext(); + + return entries[keyPosition() + 1]; + } + } + + /** Iterator over entries which supports access to unboxed keys and values. */ + public final class EntryIterator extends AbstractIterator + implements Iterator>, Entry { + public Integer getKey() { + return getIntKey(); + } + + public int getIntKey() { + return entries[keyPosition()]; + } + + public Integer getValue() { + return getIntValue(); + } + + public int getIntValue() { + return entries[keyPosition() + 1]; + } + + public Integer setValue(final Integer value) { + return setValue(value.intValue()); + } + + public int setValue(final int value) { + if (!isPositionValid) { + throw new IllegalStateException(); + } + + if (missingValue == value) { + throw new IllegalArgumentException(); + } + + final int keyPosition = keyPosition(); + final int prevValue = entries[keyPosition + 1]; + entries[keyPosition + 1] = value; + return prevValue; + } + + public Entry next() { + findNext(); + + if (shouldAvoidAllocation) { + return this; + } + + return allocateDuplicateEntry(); + } + + private Entry allocateDuplicateEntry() { + final int k = getIntKey(); + final int v = getIntValue(); + + return new Entry() { + public Integer getKey() { + return k; + } + + public Integer getValue() { + return v; + } + + public Integer setValue(final Integer value) { + return Int2IntHashMap.this.put(k, value.intValue()); + } + + public int hashCode() { + return getIntKey() ^ getIntValue(); + } + + public boolean equals(final Object o) { + if (!(o instanceof Entry)) { + return false; + } + + final Entry e = (Entry) o; + + return (e.getKey() != null && e.getValue() != null) + && (e.getKey().equals(k) && e.getValue().equals(v)); + } + + public String toString() { + return k + "=" + v; + } + }; + } + + /** {@inheritDoc} */ + public int hashCode() { + return getIntKey() ^ getIntValue(); + } + + /** {@inheritDoc} */ + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Entry)) { + return false; + } + + final Entry that = (Entry) o; + + return Objects.equals(getKey(), that.getKey()) && Objects.equals(getValue(), that.getValue()); + } + } + + /** Set of keys which supports optional cached iterators to avoid allocation. */ + public final class KeySet extends AbstractSet implements Serializable { + private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; + + /** {@inheritDoc} */ + public KeyIterator iterator() { + KeyIterator keyIterator = this.keyIterator; + if (null == keyIterator) { + keyIterator = new KeyIterator(); + } + + keyIterator.reset(); + + return keyIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Int2IntHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return Int2IntHashMap.this.isEmpty(); + } + + /** {@inheritDoc} */ + public void clear() { + Int2IntHashMap.this.clear(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + return contains((int) o); + } + + public boolean contains(final int key) { + return containsKey(key); + } + } + + /** Collection of values which supports optionally cached iterators to avoid allocation. */ + public final class ValueCollection extends AbstractCollection { + private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; + + /** {@inheritDoc} */ + public ValueIterator iterator() { + ValueIterator valueIterator = this.valueIterator; + if (null == valueIterator) { + valueIterator = new ValueIterator(); + } + + valueIterator.reset(); + + return valueIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Int2IntHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + return contains((int) o); + } + + public boolean contains(final int key) { + return containsValue(key); + } + } + + /** Set of entries which supports optionally cached iterators to avoid allocation. */ + public final class EntrySet extends AbstractSet> implements Serializable { + private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; + + /** {@inheritDoc} */ + public EntryIterator iterator() { + EntryIterator entryIterator = this.entryIterator; + if (null == entryIterator) { + entryIterator = new EntryIterator(); + } + + entryIterator.reset(); + + return entryIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Int2IntHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return Int2IntHashMap.this.isEmpty(); + } + + /** {@inheritDoc} */ + public void clear() { + Int2IntHashMap.this.clear(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + final Entry entry = (Entry) o; + final Integer value = get(entry.getKey()); + + return value != null && value.equals(entry.getValue()); + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java new file mode 100644 index 000000000..6f01d8f2d --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java @@ -0,0 +1,790 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * {@link Map} implementation specialised for int keys using open addressing and linear probing for + * cache efficient access. + * + * @param type of values stored in the {@link Map} + */ +public class Int2ObjectHashMap implements Map, Serializable { + static final int MIN_CAPACITY = 8; + + private final float loadFactor; + private int resizeThreshold; + private int size; + private final boolean shouldAvoidAllocation; + + private int[] keys; + private Object[] values; + + private ValueCollection valueCollection; + private KeySet keySet; + private EntrySet entrySet; + + public Int2ObjectHashMap() { + this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, true); + } + + public Int2ObjectHashMap(final int initialCapacity, final float loadFactor) { + this(initialCapacity, loadFactor, true); + } + + /** + * Construct a new map allowing a configuration for initial capacity and load factor. + * + * @param initialCapacity for the backing array + * @param loadFactor limit for resizing on puts + * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. + */ + public Int2ObjectHashMap( + final int initialCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { + validateLoadFactor(loadFactor); + + this.loadFactor = loadFactor; + this.shouldAvoidAllocation = shouldAvoidAllocation; + + /* */ final int capacity = findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity)); + /* */ resizeThreshold = (int) (capacity * loadFactor); + + keys = new int[capacity]; + values = new Object[capacity]; + } + + /** + * Copy construct a new map from an existing one. + * + * @param mapToCopy for construction. + */ + public Int2ObjectHashMap(final Int2ObjectHashMap mapToCopy) { + this.loadFactor = mapToCopy.loadFactor; + this.resizeThreshold = mapToCopy.resizeThreshold; + this.size = mapToCopy.size; + this.shouldAvoidAllocation = mapToCopy.shouldAvoidAllocation; + + keys = mapToCopy.keys.clone(); + values = mapToCopy.values.clone(); + } + + /** + * Get the load factor beyond which the map will increase size. + * + * @return load factor for when the map should increase size. + */ + public float loadFactor() { + return loadFactor; + } + + /** + * Get the total capacity for the map to which the load factor will be a fraction of. + * + * @return the total capacity for the map. + */ + public int capacity() { + return values.length; + } + + /** + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. + * + * @return the threshold when the map will resize. + */ + public int resizeThreshold() { + return resizeThreshold; + } + + /** {@inheritDoc} */ + public int size() { + return size; + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return 0 == size; + } + + /** {@inheritDoc} */ + public boolean containsKey(final Object key) { + return containsKey(((Integer) key).intValue()); + } + + /** + * Overloaded version of {@link Map#containsKey(Object)} that takes a primitive int key. + * + * @param key for indexing the {@link Map} + * @return true if the key is found otherwise false. + */ + public boolean containsKey(final int key) { + final int mask = values.length - 1; + int index = Hashing.hash(key, mask); + + boolean found = false; + while (null != values[index]) { + if (key == keys[index]) { + found = true; + break; + } + + index = ++index & mask; + } + + return found; + } + + /** {@inheritDoc} */ + public boolean containsValue(final Object value) { + boolean found = false; + final Object val = mapNullValue(value); + if (null != val) { + for (final Object v : values) { + if (val.equals(v)) { + found = true; + break; + } + } + } + + return found; + } + + /** {@inheritDoc} */ + public V get(final Object key) { + return get(((Integer) key).intValue()); + } + + /** + * Overloaded version of {@link Map#get(Object)} that takes a primitive int key. + * + * @param key for indexing the {@link Map} + * @return the value if found otherwise null + */ + public V get(final int key) { + return unmapNullValue(getMapped(key)); + } + + @SuppressWarnings("unchecked") + protected V getMapped(final int key) { + final int mask = values.length - 1; + int index = Hashing.hash(key, mask); + + Object value; + while (null != (value = values[index])) { + if (key == keys[index]) { + break; + } + + index = ++index & mask; + } + + return (V) value; + } + + /** {@inheritDoc} */ + public V put(final Integer key, final V value) { + return put(key.intValue(), value); + } + + /** + * Overloaded version of {@link Map#put(Object, Object)} that takes a primitive int key. + * + * @param key for indexing the {@link Map} + * @param value to be inserted in the {@link Map} + * @return the previous value if found otherwise null + */ + @SuppressWarnings("unchecked") + public V put(final int key, final V value) { + final V val = (V) mapNullValue(value); + requireNonNull(val, "value cannot be null"); + + V oldValue = null; + final int mask = values.length - 1; + int index = Hashing.hash(key, mask); + + while (null != values[index]) { + if (key == keys[index]) { + oldValue = (V) values[index]; + break; + } + + index = ++index & mask; + } + + if (null == oldValue) { + ++size; + keys[index] = key; + } + + values[index] = val; + + if (size > resizeThreshold) { + increaseCapacity(); + } + + return unmapNullValue(oldValue); + } + + /** {@inheritDoc} */ + public V remove(final Object key) { + return remove(((Integer) key).intValue()); + } + + /** + * Overloaded version of {@link Map#remove(Object)} that takes a primitive int key. + * + * @param key for indexing the {@link Map} + * @return the value if found otherwise null + */ + public V remove(final int key) { + final int mask = values.length - 1; + int index = Hashing.hash(key, mask); + + Object value; + while (null != (value = values[index])) { + if (key == keys[index]) { + values[index] = null; + --size; + + compactChain(index); + break; + } + + index = ++index & mask; + } + + return unmapNullValue(value); + } + + /** {@inheritDoc} */ + public void clear() { + if (size > 0) { + Arrays.fill(values, null); + size = 0; + } + } + + /** + * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current + * size and giving consideration to the load factor. + */ + public void compact() { + final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); + rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); + } + + /** {@inheritDoc} */ + public void putAll(final Map map) { + for (final Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** {@inheritDoc} */ + public KeySet keySet() { + if (null == keySet) { + keySet = new KeySet(); + } + + return keySet; + } + + /** {@inheritDoc} */ + public ValueCollection values() { + if (null == valueCollection) { + valueCollection = new ValueCollection(); + } + + return valueCollection; + } + + /** {@inheritDoc} */ + public EntrySet entrySet() { + if (null == entrySet) { + entrySet = new EntrySet(); + } + + return entrySet; + } + + /** {@inheritDoc} */ + public String toString() { + if (isEmpty()) { + return "{}"; + } + + final EntryIterator entryIterator = new EntryIterator(); + entryIterator.reset(); + + final StringBuilder sb = new StringBuilder().append('{'); + while (true) { + entryIterator.next(); + sb.append(entryIterator.getIntKey()) + .append('=') + .append(unmapNullValue(entryIterator.getValue())); + if (!entryIterator.hasNext()) { + return sb.append('}').toString(); + } + sb.append(',').append(' '); + } + } + + /** {@inheritDoc} */ + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Map)) { + return false; + } + + final Map that = (Map) o; + + if (size != that.size()) { + return false; + } + + for (int i = 0, length = values.length; i < length; i++) { + final Object thisValue = values[i]; + if (null != thisValue) { + final Object thatValue = that.get(keys[i]); + if (!thisValue.equals(mapNullValue(thatValue))) { + return false; + } + } + } + + return true; + } + + /** {@inheritDoc} */ + public int hashCode() { + int result = 0; + + for (int i = 0, length = values.length; i < length; i++) { + final Object value = values[i]; + if (null != value) { + result += (keys[i] ^ value.hashCode()); + } + } + + return result; + } + + protected Object mapNullValue(final Object value) { + return value; + } + + @SuppressWarnings("unchecked") + protected V unmapNullValue(final Object value) { + return (V) value; + } + + /** + * Primitive specialised version of {@link #replace(Object, Object)} + * + * @param key key with which the specified value is associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or {@code null} if there was no + * mapping for the key. + */ + public V replace(final int key, final V value) { + V curValue = get(key); + if (curValue != null) { + curValue = put(key, value); + } + + return curValue; + } + + /** + * Primitive specialised version of {@link #replace(Object, Object, Object)} + * + * @param key key with which the specified value is associated + * @param oldValue value expected to be associated with the specified key + * @param newValue value to be associated with the specified key + * @return {@code true} if the value was replaced + */ + public boolean replace(final int key, final V oldValue, final V newValue) { + final Object curValue = get(key); + if (curValue == null || !Objects.equals(unmapNullValue(curValue), oldValue)) { + return false; + } + + put(key, newValue); + + return true; + } + + private void increaseCapacity() { + final int newCapacity = values.length << 1; + if (newCapacity < 0) { + throw new IllegalStateException("max capacity reached at size=" + size); + } + + rehash(newCapacity); + } + + private void rehash(final int newCapacity) { + final int mask = newCapacity - 1; + /* */ resizeThreshold = (int) (newCapacity * loadFactor); + + final int[] tempKeys = new int[newCapacity]; + final Object[] tempValues = new Object[newCapacity]; + + for (int i = 0, size = values.length; i < size; i++) { + final Object value = values[i]; + if (null != value) { + final int key = keys[i]; + int index = Hashing.hash(key, mask); + while (null != tempValues[index]) { + index = ++index & mask; + } + + tempKeys[index] = key; + tempValues[index] = value; + } + } + + keys = tempKeys; + values = tempValues; + } + + @SuppressWarnings("FinalParameters") + private void compactChain(int deleteIndex) { + final int mask = values.length - 1; + int index = deleteIndex; + while (true) { + index = ++index & mask; + if (null == values[index]) { + break; + } + + final int hash = Hashing.hash(keys[index], mask); + + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) + || (hash <= deleteIndex && deleteIndex <= index)) { + keys[deleteIndex] = keys[index]; + values[deleteIndex] = values[index]; + + values[index] = null; + deleteIndex = index; + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Sets and Collections + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** Set of keys which supports optionally cached iterators to avoid allocation. */ + public final class KeySet extends AbstractSet implements Serializable { + private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; + + /** {@inheritDoc} */ + public KeyIterator iterator() { + KeyIterator keyIterator = this.keyIterator; + if (null == keyIterator) { + keyIterator = new KeyIterator(); + } + + keyIterator.reset(); + return keyIterator; + } + + public int size() { + return Int2ObjectHashMap.this.size(); + } + + public boolean contains(final Object o) { + return Int2ObjectHashMap.this.containsKey(o); + } + + public boolean contains(final int key) { + return Int2ObjectHashMap.this.containsKey(key); + } + + public boolean remove(final Object o) { + return null != Int2ObjectHashMap.this.remove(o); + } + + public boolean remove(final int key) { + return null != Int2ObjectHashMap.this.remove(key); + } + + public void clear() { + Int2ObjectHashMap.this.clear(); + } + } + + /** Collection of values which supports optionally cached iterators to avoid allocation. */ + public final class ValueCollection extends AbstractCollection implements Serializable { + private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; + + /** {@inheritDoc} */ + public ValueIterator iterator() { + ValueIterator valueIterator = this.valueIterator; + if (null == valueIterator) { + valueIterator = new ValueIterator(); + } + + valueIterator.reset(); + return valueIterator; + } + + public int size() { + return Int2ObjectHashMap.this.size(); + } + + public boolean contains(final Object o) { + return Int2ObjectHashMap.this.containsValue(o); + } + + public void clear() { + Int2ObjectHashMap.this.clear(); + } + } + + /** Set of entries which supports access via an optionally cached iterator to avoid allocation. */ + public final class EntrySet extends AbstractSet> implements Serializable { + private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; + + /** {@inheritDoc} */ + public EntryIterator iterator() { + EntryIterator entryIterator = this.entryIterator; + if (null == entryIterator) { + entryIterator = new EntryIterator(); + } + + entryIterator.reset(); + return entryIterator; + } + + public int size() { + return Int2ObjectHashMap.this.size(); + } + + public void clear() { + Int2ObjectHashMap.this.clear(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + final Entry entry = (Entry) o; + final int key = (Integer) entry.getKey(); + final V value = getMapped(key); + return value != null && value.equals(mapNullValue(entry.getValue())); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Iterators + /////////////////////////////////////////////////////////////////////////////////////////////// + + abstract class AbstractIterator implements Iterator, Serializable { + private int posCounter; + private int stopCounter; + private int remaining; + boolean isPositionValid = false; + + protected final int position() { + return posCounter & (values.length - 1); + } + + public int remaining() { + return remaining; + } + + public boolean hasNext() { + return remaining > 0; + } + + protected final void findNext() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + final Object[] values = Int2ObjectHashMap.this.values; + final int mask = values.length - 1; + + for (int i = posCounter - 1; i >= stopCounter; i--) { + final int index = i & mask; + if (null != values[index]) { + posCounter = i; + isPositionValid = true; + --remaining; + return; + } + } + + isPositionValid = false; + throw new IllegalStateException(); + } + + public abstract T next(); + + public void remove() { + if (isPositionValid) { + final int position = position(); + values[position] = null; + --size; + + compactChain(position); + + isPositionValid = false; + } else { + throw new IllegalStateException(); + } + } + + final void reset() { + remaining = Int2ObjectHashMap.this.size; + final Object[] values = Int2ObjectHashMap.this.values; + final int capacity = values.length; + + int i = capacity; + if (null != values[capacity - 1]) { + for (i = 0; i < capacity; i++) { + if (null == values[i]) { + break; + } + } + } + + stopCounter = i; + posCounter = i + capacity; + isPositionValid = false; + } + } + + /** Iterator over values. */ + public class ValueIterator extends AbstractIterator { + public V next() { + findNext(); + + return unmapNullValue(values[position()]); + } + } + + /** Iterator over keys which supports access to unboxed keys. */ + public class KeyIterator extends AbstractIterator { + public Integer next() { + return nextInt(); + } + + public int nextInt() { + findNext(); + + return keys[position()]; + } + } + + /** Iterator over entries which supports access to unboxed keys and values. */ + public class EntryIterator extends AbstractIterator> + implements Entry { + public Entry next() { + findNext(); + if (shouldAvoidAllocation) { + return this; + } + + return allocateDuplicateEntry(); + } + + private Entry allocateDuplicateEntry() { + final int k = getIntKey(); + final V v = getValue(); + + return new Entry() { + public Integer getKey() { + return k; + } + + public V getValue() { + return v; + } + + public V setValue(final V value) { + return Int2ObjectHashMap.this.put(k, value); + } + + public int hashCode() { + return getIntKey() ^ (v != null ? v.hashCode() : 0); + } + + public boolean equals(final Object o) { + if (!(o instanceof Entry)) { + return false; + } + + final Entry e = (Entry) o; + + return (e.getKey() != null && e.getKey().equals(k)) + && ((e.getValue() == null && v == null) || e.getValue().equals(v)); + } + + public String toString() { + return k + "=" + v; + } + }; + } + + public Integer getKey() { + return getIntKey(); + } + + public int getIntKey() { + return keys[position()]; + } + + public V getValue() { + return unmapNullValue(values[position()]); + } + + @SuppressWarnings("unchecked") + public V setValue(final V value) { + final V val = (V) mapNullValue(value); + requireNonNull(val, "value cannot be null"); + + if (!this.isPositionValid) { + throw new IllegalStateException(); + } + + final int pos = position(); + final Object oldValue = values[pos]; + values[pos] = val; + + return (V) oldValue; + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java new file mode 100644 index 000000000..cde2b72f8 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +/** This is an (int, int) primitive specialisation of a BiConsumer */ +@FunctionalInterface +public interface IntIntConsumer { + /** + * Accept two values that comes as a tuple of ints. + * + * @param valueOne for the tuple. + * @param valueTwo for the tuple. + */ + void accept(int valueOne, int valueTwo); +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java new file mode 100644 index 000000000..aaf221fd6 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java @@ -0,0 +1,864 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** A open addressing with linear probing hash map specialised for primitive key and value pairs. */ +public class Long2LongHashMap implements Map, Serializable { + static final int MIN_CAPACITY = 8; + + private final float loadFactor; + private final long missingValue; + private int resizeThreshold; + private int size = 0; + private final boolean shouldAvoidAllocation; + + private long[] entries; + private KeySet keySet; + private ValueCollection values; + private EntrySet entrySet; + + public Long2LongHashMap(final long missingValue) { + this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, missingValue); + } + + public Long2LongHashMap( + final int initialCapacity, final float loadFactor, final long missingValue) { + this(initialCapacity, loadFactor, missingValue, true); + } + + /** + * @param initialCapacity for the map to override {@link #MIN_CAPACITY} + * @param loadFactor for the map to override {@link Hashing#DEFAULT_LOAD_FACTOR}. + * @param missingValue for the map that represents null. + * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. + */ + public Long2LongHashMap( + final int initialCapacity, + final float loadFactor, + final long missingValue, + final boolean shouldAvoidAllocation) { + validateLoadFactor(loadFactor); + + this.loadFactor = loadFactor; + this.missingValue = missingValue; + this.shouldAvoidAllocation = shouldAvoidAllocation; + + capacity(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity))); + } + + /** + * The value to be used as a null marker in the map. + * + * @return value to be used as a null marker in the map. + */ + public long missingValue() { + return missingValue; + } + + /** + * Get the load factor applied for resize operations. + * + * @return the load factor applied for resize operations. + */ + public float loadFactor() { + return loadFactor; + } + + /** + * Get the total capacity for the map to which the load factor will be a fraction of. + * + * @return the total capacity for the map. + */ + public int capacity() { + return entries.length >> 2; + } + + /** + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. + * + * @return the threshold when the map will resize. + */ + public int resizeThreshold() { + return resizeThreshold; + } + + /** {@inheritDoc} */ + public int size() { + return size; + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return size == 0; + } + + public long get(final long key) { + final long[] entries = this.entries; + final long missingValue = this.missingValue; + final int mask = entries.length - 1; + int index = Hashing.evenHash(key, mask); + + long value = missingValue; + while (entries[index + 1] != missingValue) { + if (entries[index] == key) { + value = entries[index + 1]; + break; + } + + index = next(index, mask); + } + + return value; + } + + /** + * Put a key value pair in the map. + * + * @param key lookup key + * @param value new value, must not be initialValue + * @return current counter value associated with key, or initialValue if none found + * @throws IllegalArgumentException if value is missingValue + */ + public long put(final long key, final long value) { + if (value == missingValue) { + throw new IllegalArgumentException("cannot accept missingValue"); + } + + final long[] entries = this.entries; + final long missingValue = this.missingValue; + final int mask = entries.length - 1; + int index = Hashing.evenHash(key, mask); + long oldValue = missingValue; + + while (entries[index + 1] != missingValue) { + if (entries[index] == key) { + oldValue = entries[index + 1]; + break; + } + + index = next(index, mask); + } + + if (oldValue == missingValue) { + ++size; + entries[index] = key; + } + + entries[index + 1] = value; + + increaseCapacity(); + + return oldValue; + } + + private void increaseCapacity() { + if (size > resizeThreshold) { + // entries.length = 2 * capacity + final int newCapacity = entries.length; + rehash(newCapacity); + } + } + + private void rehash(final int newCapacity) { + final long[] oldEntries = entries; + final long missingValue = this.missingValue; + final int length = entries.length; + + capacity(newCapacity); + + final long[] newEntries = entries; + final int mask = entries.length - 1; + + for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { + final long value = oldEntries[keyIndex + 1]; + if (value != missingValue) { + final long key = oldEntries[keyIndex]; + int index = Hashing.evenHash(key, mask); + + while (newEntries[index + 1] != missingValue) { + index = next(index, mask); + } + + newEntries[index] = key; + newEntries[index + 1] = value; + } + } + } + + /** + * Primitive specialised forEach implementation. + * + *

NB: Renamed from forEach to avoid overloading on parameter types of lambda expression, which + * doesn't play well with type inference in lambda expressions. + * + * @param consumer a callback called for each key/value pair in the map. + */ + public void longForEach(final LongLongConsumer consumer) { + final long[] entries = this.entries; + final long missingValue = this.missingValue; + final int length = entries.length; + + for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { + if (entries[keyIndex + 1] != missingValue) // lgtm [java/index-out-of-bounds] + { + consumer.accept( + entries[keyIndex], entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] + } + } + } + + /** + * Long primitive specialised containsKey. + * + * @param key the key to check. + * @return true if the map contains key as a key, false otherwise. + */ + public boolean containsKey(final long key) { + return get(key) != missingValue; + } + + /** + * Does the map contain the value. + * + * @param value to be tested against contained values. + * @return true if contained otherwise value. + */ + public boolean containsValue(final long value) { + boolean found = false; + if (value != missingValue) { + final long[] entries = this.entries; + final int length = entries.length; + + for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { + if (value == entries[valueIndex]) { + found = true; + break; + } + } + } + + return found; + } + + /** {@inheritDoc} */ + public void clear() { + if (size > 0) { + Arrays.fill(entries, missingValue); + size = 0; + } + } + + /** + * Compact the backing arrays by rehashing with a capacity just larger than current size and + * giving consideration to the load factor. + */ + public void compact() { + final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); + rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); + } + + // ---------------- Boxed Versions Below ---------------- + + /** {@inheritDoc} */ + public Long get(final Object key) { + return valOrNull(get((long) key)); + } + + /** {@inheritDoc} */ + public Long put(final Long key, final Long value) { + return valOrNull(put((long) key, (long) value)); + } + + /** {@inheritDoc} */ + public boolean containsKey(final Object key) { + return containsKey((long) key); + } + + /** {@inheritDoc} */ + public boolean containsValue(final Object value) { + return containsValue((long) value); + } + + /** {@inheritDoc} */ + public void putAll(final Map map) { + for (final Map.Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** {@inheritDoc} */ + public KeySet keySet() { + if (null == keySet) { + keySet = new KeySet(); + } + + return keySet; + } + + /** {@inheritDoc} */ + public ValueCollection values() { + if (null == values) { + values = new ValueCollection(); + } + + return values; + } + + /** {@inheritDoc} */ + public EntrySet entrySet() { + if (null == entrySet) { + entrySet = new EntrySet(); + } + + return entrySet; + } + + /** {@inheritDoc} */ + public Long remove(final Object key) { + return valOrNull(remove((long) key)); + } + + public long remove(final long key) { + final long[] entries = this.entries; + final long missingValue = this.missingValue; + final int mask = entries.length - 1; + int keyIndex = Hashing.evenHash(key, mask); + + long oldValue = missingValue; + while (entries[keyIndex + 1] != missingValue) { + if (entries[keyIndex] == key) { + oldValue = entries[keyIndex + 1]; + entries[keyIndex + 1] = missingValue; + size--; + + compactChain(keyIndex); + + break; + } + + keyIndex = next(keyIndex, mask); + } + + return oldValue; + } + + @SuppressWarnings("FinalParameters") + private void compactChain(int deleteKeyIndex) { + final long[] entries = this.entries; + final long missingValue = this.missingValue; + final int mask = entries.length - 1; + int keyIndex = deleteKeyIndex; + + while (true) { + keyIndex = next(keyIndex, mask); + if (entries[keyIndex + 1] == missingValue) { + break; + } + + final int hash = Hashing.evenHash(entries[keyIndex], mask); + + if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) + || (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { + entries[deleteKeyIndex] = entries[keyIndex]; + entries[deleteKeyIndex + 1] = entries[keyIndex + 1]; + + entries[keyIndex + 1] = missingValue; + deleteKeyIndex = keyIndex; + } + } + } + + /** + * Get the minimum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} + * + * @return the minimum value stored in the map. + */ + public long minValue() { + final long missingValue = this.missingValue; + long min = size == 0 ? missingValue : Long.MAX_VALUE; + + final long[] entries = this.entries; + final int length = entries.length; + + for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { + final long value = entries[valueIndex]; + if (value != missingValue) { + min = Math.min(min, value); + } + } + + return min; + } + + /** + * Get the maximum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} + * + * @return the maximum value stored in the map. + */ + public long maxValue() { + final long missingValue = this.missingValue; + long max = size == 0 ? missingValue : Long.MIN_VALUE; + + final long[] entries = this.entries; + final int length = entries.length; + + for (int valueIndex = 1; valueIndex < length; valueIndex += 2) { + final long value = entries[valueIndex]; + if (value != missingValue) { + max = Math.max(max, value); + } + } + + return max; + } + + /** {@inheritDoc} */ + public String toString() { + if (isEmpty()) { + return "{}"; + } + + final EntryIterator entryIterator = new EntryIterator(); + entryIterator.reset(); + + final StringBuilder sb = new StringBuilder().append('{'); + while (true) { + entryIterator.next(); + sb.append(entryIterator.getLongKey()).append('=').append(entryIterator.getLongValue()); + if (!entryIterator.hasNext()) { + return sb.append('}').toString(); + } + sb.append(',').append(' '); + } + } + + /** + * Primitive specialised version of {@link #replace(Object, Object)} + * + * @param key key with which the specified value is associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or {@link #missingValue()} if + * there was no mapping for the key. + */ + public long replace(final long key, final long value) { + long currentValue = get(key); + if (currentValue != missingValue) { + currentValue = put(key, value); + } + + return currentValue; + } + + /** + * Primitive specialised version of {@link #replace(Object, Object, Object)} + * + * @param key key with which the specified value is associated + * @param oldValue value expected to be associated with the specified key + * @param newValue value to be associated with the specified key + * @return {@code true} if the value was replaced + */ + public boolean replace(final long key, final long oldValue, final long newValue) { + final long curValue = get(key); + if (curValue != oldValue || curValue == missingValue) { + return false; + } + + put(key, newValue); + + return true; + } + + /** {@inheritDoc} */ + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Map)) { + return false; + } + + final Map that = (Map) o; + + return size == that.size() && entrySet().equals(that.entrySet()); + } + + public int hashCode() { + return entrySet().hashCode(); + } + + private static int next(final int index, final int mask) { + return (index + 2) & mask; + } + + private void capacity(final int newCapacity) { + final int entriesLength = newCapacity * 2; + if (entriesLength < 0) { + throw new IllegalStateException("max capacity reached at size=" + size); + } + + resizeThreshold = (int) (newCapacity * loadFactor); + entries = new long[entriesLength]; + Arrays.fill(entries, missingValue); + } + + private Long valOrNull(final long value) { + return value == missingValue ? null : value; + } + + // ---------------- Utility Classes ---------------- + + abstract class AbstractIterator implements Serializable { + protected boolean isPositionValid = false; + private int remaining; + private int positionCounter; + private int stopCounter; + + final void reset() { + isPositionValid = false; + remaining = Long2LongHashMap.this.size; + final long missingValue = Long2LongHashMap.this.missingValue; + final long[] entries = Long2LongHashMap.this.entries; + final int capacity = entries.length; + + int keyIndex = capacity; + if (entries[capacity - 1] != missingValue) { + keyIndex = 0; + for (; keyIndex < capacity; keyIndex += 2) { + if (entries[keyIndex + 1] == missingValue) // lgtm [java/index-out-of-bounds] + { + break; + } + } + } + + stopCounter = keyIndex; + positionCounter = keyIndex + capacity; + } + + protected final int keyPosition() { + return positionCounter & entries.length - 1; + } + + public int remaining() { + return remaining; + } + + public boolean hasNext() { + return remaining > 0; + } + + protected final void findNext() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + final long[] entries = Long2LongHashMap.this.entries; + final long missingValue = Long2LongHashMap.this.missingValue; + final int mask = entries.length - 1; + + for (int keyIndex = positionCounter - 2; keyIndex >= stopCounter; keyIndex -= 2) { + final int index = keyIndex & mask; + if (entries[index + 1] != missingValue) { + isPositionValid = true; + positionCounter = keyIndex; + --remaining; + return; + } + } + + isPositionValid = false; + throw new IllegalStateException(); + } + + public void remove() { + if (isPositionValid) { + final int position = keyPosition(); + entries[position + 1] = missingValue; + --size; + + compactChain(position); + + isPositionValid = false; + } else { + throw new IllegalStateException(); + } + } + } + + /** Iterator over keys which supports access to unboxed keys. */ + public final class KeyIterator extends AbstractIterator implements Iterator { + public Long next() { + return nextValue(); + } + + public long nextValue() { + findNext(); + return entries[keyPosition()]; + } + } + + /** Iterator over values which supports access to unboxed values. */ + public final class ValueIterator extends AbstractIterator implements Iterator { + public Long next() { + return nextValue(); + } + + public long nextValue() { + findNext(); + return entries[keyPosition() + 1]; + } + } + + /** Iterator over entries which supports access to unboxed keys and values. */ + public final class EntryIterator extends AbstractIterator + implements Iterator>, Entry { + public Long getKey() { + return getLongKey(); + } + + public long getLongKey() { + return entries[keyPosition()]; + } + + public Long getValue() { + return getLongValue(); + } + + public long getLongValue() { + return entries[keyPosition() + 1]; + } + + public Long setValue(final Long value) { + return setValue(value.longValue()); + } + + public long setValue(final long value) { + if (!isPositionValid) { + throw new IllegalStateException(); + } + + if (missingValue == value) { + throw new IllegalArgumentException(); + } + + final int keyPosition = keyPosition(); + final long prevValue = entries[keyPosition + 1]; + entries[keyPosition + 1] = value; + return prevValue; + } + + public Entry next() { + findNext(); + + if (shouldAvoidAllocation) { + return this; + } + + return allocateDuplicateEntry(); + } + + private Entry allocateDuplicateEntry() { + final long k = getLongKey(); + final long v = getLongValue(); + + return new Entry() { + public Long getKey() { + return k; + } + + public Long getValue() { + return v; + } + + public Long setValue(final Long value) { + return Long2LongHashMap.this.put(k, value.longValue()); + } + + public int hashCode() { + return Hashing.hashCode(getLongKey()) ^ Hashing.hashCode(getLongValue()); + } + + public boolean equals(final Object o) { + if (!(o instanceof Entry)) { + return false; + } + + final Map.Entry e = (Entry) o; + + return (e.getKey() != null && e.getValue() != null) + && (e.getKey().equals(k) && e.getValue().equals(v)); + } + + public String toString() { + return k + "=" + v; + } + }; + } + + /** {@inheritDoc} */ + public int hashCode() { + return Hashing.hashCode(getLongKey()) ^ Hashing.hashCode(getLongValue()); + } + + /** {@inheritDoc} */ + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Entry)) { + return false; + } + + final Entry that = (Entry) o; + + return Objects.equals(getKey(), that.getKey()) && Objects.equals(getValue(), that.getValue()); + } + } + + /** Set of keys which supports optional cached iterators to avoid allocation. */ + public final class KeySet extends AbstractSet implements Serializable { + private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; + + /** {@inheritDoc} */ + public KeyIterator iterator() { + KeyIterator keyIterator = this.keyIterator; + if (null == keyIterator) { + keyIterator = new KeyIterator(); + } + + keyIterator.reset(); + + return keyIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Long2LongHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return Long2LongHashMap.this.isEmpty(); + } + + /** {@inheritDoc} */ + public void clear() { + Long2LongHashMap.this.clear(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + return contains((long) o); + } + + public boolean contains(final long key) { + return containsKey(key); + } + } + + /** Collection of values which supports optionally cached iterators to avoid allocation. */ + public final class ValueCollection extends AbstractCollection { + private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; + + /** {@inheritDoc} */ + public ValueIterator iterator() { + ValueIterator valueIterator = this.valueIterator; + if (null == valueIterator) { + valueIterator = new ValueIterator(); + } + + valueIterator.reset(); + + return valueIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Long2LongHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + return contains((long) o); + } + + public boolean contains(final long key) { + return containsValue(key); + } + } + + /** Set of entries which supports optionally cached iterators to avoid allocation. */ + public final class EntrySet extends AbstractSet> implements Serializable { + private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; + + /** {@inheritDoc} */ + public EntryIterator iterator() { + EntryIterator entryIterator = this.entryIterator; + if (null == entryIterator) { + entryIterator = new EntryIterator(); + } + + entryIterator.reset(); + + return entryIterator; + } + + /** {@inheritDoc} */ + public int size() { + return Long2LongHashMap.this.size(); + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return Long2LongHashMap.this.isEmpty(); + } + + /** {@inheritDoc} */ + public void clear() { + Long2LongHashMap.this.clear(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + if (!(o instanceof Entry)) { + return false; + } + final Entry entry = (Entry) o; + final Long value = get(entry.getKey()); + + return value != null && value.equals(entry.getValue()); + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java new file mode 100644 index 000000000..809f08566 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java @@ -0,0 +1,789 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; +import static java.util.Objects.requireNonNull; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * {@link Map} implementation specialised for long keys using open addressing and linear probing for + * cache efficient access. + * + * @param type of values stored in the {@link Map} + */ +public class Long2ObjectHashMap implements Map, Serializable { + static final int MIN_CAPACITY = 8; + + private final float loadFactor; + private int resizeThreshold; + private int size; + private final boolean shouldAvoidAllocation; + + private long[] keys; + private Object[] values; + + private ValueCollection valueCollection; + private KeySet keySet; + private EntrySet entrySet; + + public Long2ObjectHashMap() { + this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, true); + } + + public Long2ObjectHashMap(final int initialCapacity, final float loadFactor) { + this(initialCapacity, loadFactor, true); + } + + /** + * Construct a new map allowing a configuration for initial capacity and load factor. + * + * @param initialCapacity for the backing array + * @param loadFactor limit for resizing on puts + * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. + */ + public Long2ObjectHashMap( + final int initialCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { + validateLoadFactor(loadFactor); + + this.loadFactor = loadFactor; + this.shouldAvoidAllocation = shouldAvoidAllocation; + + /* */ final int capacity = findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity)); + /* */ resizeThreshold = (int) (capacity * loadFactor); + + keys = new long[capacity]; + values = new Object[capacity]; + } + + /** + * Copy construct a new map from an existing one. + * + * @param mapToCopy for construction. + */ + public Long2ObjectHashMap(final Long2ObjectHashMap mapToCopy) { + this.loadFactor = mapToCopy.loadFactor; + this.resizeThreshold = mapToCopy.resizeThreshold; + this.size = mapToCopy.size; + this.shouldAvoidAllocation = mapToCopy.shouldAvoidAllocation; + + keys = mapToCopy.keys.clone(); + values = mapToCopy.values.clone(); + } + + /** + * Get the load factor beyond which the map will increase size. + * + * @return load factor for when the map should increase size. + */ + public float loadFactor() { + return loadFactor; + } + + /** + * Get the total capacity for the map to which the load factor will be a fraction of. + * + * @return the total capacity for the map. + */ + public int capacity() { + return values.length; + } + + /** + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. + * + * @return the threshold when the map will resize. + */ + public int resizeThreshold() { + return resizeThreshold; + } + + /** {@inheritDoc} */ + public int size() { + return size; + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return 0 == size; + } + + /** {@inheritDoc} */ + public boolean containsKey(final Object key) { + return containsKey(((Long) key).longValue()); + } + + /** + * Overloaded version of {@link Map#containsKey(Object)} that takes a primitive long key. + * + * @param key for indexing the {@link Map} + * @return true if the key is found otherwise false. + */ + public boolean containsKey(final long key) { + final int mask = values.length - 1; + int index = Hashing.hash(key, mask); + + boolean found = false; + while (null != values[index]) { + if (key == keys[index]) { + found = true; + break; + } + + index = ++index & mask; + } + + return found; + } + + /** {@inheritDoc} */ + public boolean containsValue(final Object value) { + boolean found = false; + final Object val = mapNullValue(value); + if (null != val) { + for (final Object v : values) { + if (val.equals(v)) { + found = true; + break; + } + } + } + + return found; + } + + /** {@inheritDoc} */ + public V get(final Object key) { + return get(((Long) key).longValue()); + } + + /** + * Overloaded version of {@link Map#get(Object)} that takes a primitive long key. + * + * @param key for indexing the {@link Map} + * @return the value if found otherwise null + */ + public V get(final long key) { + return unmapNullValue(getMapped(key)); + } + + @SuppressWarnings("unchecked") + protected V getMapped(final long key) { + final int mask = values.length - 1; + int index = Hashing.hash(key, mask); + + Object value; + while (null != (value = values[index])) { + if (key == keys[index]) { + break; + } + + index = ++index & mask; + } + + return (V) value; + } + + /** {@inheritDoc} */ + public V put(final Long key, final V value) { + return put(key.longValue(), value); + } + + /** + * Overloaded version of {@link Map#put(Object, Object)} that takes a primitive long key. + * + * @param key for indexing the {@link Map} + * @param value to be inserted in the {@link Map} + * @return the previous value if found otherwise null + */ + @SuppressWarnings("unchecked") + public V put(final long key, final V value) { + final V val = (V) mapNullValue(value); + requireNonNull(val, "value cannot be null"); + + V oldValue = null; + final int mask = values.length - 1; + int index = Hashing.hash(key, mask); + + while (null != values[index]) { + if (key == keys[index]) { + oldValue = (V) values[index]; + break; + } + + index = ++index & mask; + } + + if (null == oldValue) { + ++size; + keys[index] = key; + } + + values[index] = val; + + if (size > resizeThreshold) { + increaseCapacity(); + } + + return unmapNullValue(oldValue); + } + + /** {@inheritDoc} */ + public V remove(final Object key) { + return remove(((Long) key).longValue()); + } + + /** + * Overloaded version of {@link Map#remove(Object)} that takes a primitive long key. + * + * @param key for indexing the {@link Map} + * @return the value if found otherwise null + */ + public V remove(final long key) { + final int mask = values.length - 1; + int index = Hashing.hash(key, mask); + + Object value; + while (null != (value = values[index])) { + if (key == keys[index]) { + values[index] = null; + --size; + + compactChain(index); + break; + } + + index = ++index & mask; + } + + return unmapNullValue(value); + } + + /** {@inheritDoc} */ + public void clear() { + if (size > 0) { + Arrays.fill(values, null); + size = 0; + } + } + + /** + * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current + * size and giving consideration to the load factor. + */ + public void compact() { + final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); + rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); + } + + /** {@inheritDoc} */ + public void putAll(final Map map) { + for (final Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** {@inheritDoc} */ + public KeySet keySet() { + if (null == keySet) { + keySet = new KeySet(); + } + + return keySet; + } + + /** {@inheritDoc} */ + public ValueCollection values() { + if (null == valueCollection) { + valueCollection = new ValueCollection(); + } + + return valueCollection; + } + + /** {@inheritDoc} */ + public EntrySet entrySet() { + if (null == entrySet) { + entrySet = new EntrySet(); + } + + return entrySet; + } + + /** {@inheritDoc} */ + public String toString() { + if (isEmpty()) { + return "{}"; + } + + final EntryIterator entryIterator = new EntryIterator(); + entryIterator.reset(); + + final StringBuilder sb = new StringBuilder().append('{'); + while (true) { + entryIterator.next(); + sb.append(entryIterator.getLongKey()) + .append('=') + .append(unmapNullValue(entryIterator.getValue())); + if (!entryIterator.hasNext()) { + return sb.append('}').toString(); + } + sb.append(',').append(' '); + } + } + + /** {@inheritDoc} */ + public boolean equals(final Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof Map)) { + return false; + } + + final Map that = (Map) o; + + if (size != that.size()) { + return false; + } + + for (int i = 0, length = values.length; i < length; i++) { + final Object thisValue = values[i]; + if (null != thisValue) { + final Object thatValue = that.get(keys[i]); + if (!thisValue.equals(mapNullValue(thatValue))) { + return false; + } + } + } + + return true; + } + + /** {@inheritDoc} */ + public int hashCode() { + int result = 0; + + for (int i = 0, length = values.length; i < length; i++) { + final Object value = values[i]; + if (null != value) { + result += (Hashing.hashCode(keys[i]) ^ value.hashCode()); + } + } + + return result; + } + + protected Object mapNullValue(final Object value) { + return value; + } + + @SuppressWarnings("unchecked") + protected V unmapNullValue(final Object value) { + return (V) value; + } + + /** + * Primitive specialised version of {@link #replace(Object, Object)} + * + * @param key key with which the specified value is associated + * @param value value to be associated with the specified key + * @return the previous value associated with the specified key, or {@code null} if there was no + * mapping for the key. + */ + public V replace(final long key, final V value) { + V curValue = get(key); + if (curValue != null) { + curValue = put(key, value); + } + + return curValue; + } + + /** + * Primitive specialised version of {@link #replace(Object, Object, Object)} + * + * @param key key with which the specified value is associated + * @param oldValue value expected to be associated with the specified key + * @param newValue value to be associated with the specified key + * @return {@code true} if the value was replaced + */ + public boolean replace(final long key, final V oldValue, final V newValue) { + final Object curValue = get(key); + if (curValue == null || !Objects.equals(unmapNullValue(curValue), oldValue)) { + return false; + } + + put(key, newValue); + + return true; + } + + private void increaseCapacity() { + final int newCapacity = values.length << 1; + if (newCapacity < 0) { + throw new IllegalStateException("max capacity reached at size=" + size); + } + + rehash(newCapacity); + } + + private void rehash(final int newCapacity) { + final int mask = newCapacity - 1; + /* */ resizeThreshold = (int) (newCapacity * loadFactor); + + final long[] tempKeys = new long[newCapacity]; + final Object[] tempValues = new Object[newCapacity]; + + for (int i = 0, size = values.length; i < size; i++) { + final Object value = values[i]; + if (null != value) { + final long key = keys[i]; + int index = Hashing.hash(key, mask); + while (null != tempValues[index]) { + index = ++index & mask; + } + + tempKeys[index] = key; + tempValues[index] = value; + } + } + + keys = tempKeys; + values = tempValues; + } + + @SuppressWarnings("FinalParameters") + private void compactChain(int deleteIndex) { + final int mask = values.length - 1; + int index = deleteIndex; + while (true) { + index = ++index & mask; + if (null == values[index]) { + break; + } + + final int hash = Hashing.hash(keys[index], mask); + + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) + || (hash <= deleteIndex && deleteIndex <= index)) { + keys[deleteIndex] = keys[index]; + values[deleteIndex] = values[index]; + + values[index] = null; + deleteIndex = index; + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Sets and Collections + /////////////////////////////////////////////////////////////////////////////////////////////// + + /** Set of keys which supports optionally cached iterators to avoid allocation. */ + public final class KeySet extends AbstractSet implements Serializable { + private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; + + /** {@inheritDoc} */ + public KeyIterator iterator() { + KeyIterator keyIterator = this.keyIterator; + if (null == keyIterator) { + keyIterator = new KeyIterator(); + } + + keyIterator.reset(); + return keyIterator; + } + + public int size() { + return Long2ObjectHashMap.this.size(); + } + + public boolean contains(final Object o) { + return Long2ObjectHashMap.this.containsKey(o); + } + + public boolean contains(final long key) { + return Long2ObjectHashMap.this.containsKey(key); + } + + public boolean remove(final Object o) { + return null != Long2ObjectHashMap.this.remove(o); + } + + public boolean remove(final long key) { + return null != Long2ObjectHashMap.this.remove(key); + } + + public void clear() { + Long2ObjectHashMap.this.clear(); + } + } + + /** Collection of values which supports optionally cached iterators to avoid allocation. */ + public final class ValueCollection extends AbstractCollection implements Serializable { + private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; + + /** {@inheritDoc} */ + public ValueIterator iterator() { + ValueIterator valueIterator = this.valueIterator; + if (null == valueIterator) { + valueIterator = new ValueIterator(); + } + + valueIterator.reset(); + return valueIterator; + } + + public int size() { + return Long2ObjectHashMap.this.size(); + } + + public boolean contains(final Object o) { + return Long2ObjectHashMap.this.containsValue(o); + } + + public void clear() { + Long2ObjectHashMap.this.clear(); + } + } + + /** Set of entries which supports access via an optionally cached iterator to avoid allocation. */ + public final class EntrySet extends AbstractSet> implements Serializable { + private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; + + /** {@inheritDoc} */ + public EntryIterator iterator() { + EntryIterator entryIterator = this.entryIterator; + if (null == entryIterator) { + entryIterator = new EntryIterator(); + } + + entryIterator.reset(); + return entryIterator; + } + + public int size() { + return Long2ObjectHashMap.this.size(); + } + + public void clear() { + Long2ObjectHashMap.this.clear(); + } + + /** {@inheritDoc} */ + public boolean contains(final Object o) { + final Entry entry = (Entry) o; + final long key = (Long) entry.getKey(); + final V value = getMapped(key); + return value != null && value.equals(mapNullValue(entry.getValue())); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////// + // Iterators + /////////////////////////////////////////////////////////////////////////////////////////////// + + abstract class AbstractIterator implements Iterator, Serializable { + private int posCounter; + private int stopCounter; + private int remaining; + boolean isPositionValid = false; + + protected final int position() { + return posCounter & (values.length - 1); + } + + public int remaining() { + return remaining; + } + + public boolean hasNext() { + return remaining > 0; + } + + protected final void findNext() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + final Object[] values = Long2ObjectHashMap.this.values; + final int mask = values.length - 1; + + for (int i = posCounter - 1; i >= stopCounter; i--) { + final int index = i & mask; + if (null != values[index]) { + posCounter = i; + isPositionValid = true; + --remaining; + return; + } + } + + isPositionValid = false; + throw new IllegalStateException(); + } + + public abstract T next(); + + public void remove() { + if (isPositionValid) { + final int position = position(); + values[position] = null; + --size; + + compactChain(position); + + isPositionValid = false; + } else { + throw new IllegalStateException(); + } + } + + final void reset() { + remaining = Long2ObjectHashMap.this.size; + final Object[] values = Long2ObjectHashMap.this.values; + final int capacity = values.length; + + int i = capacity; + if (null != values[capacity - 1]) { + for (i = 0; i < capacity; i++) { + if (null == values[i]) { + break; + } + } + } + + stopCounter = i; + posCounter = i + capacity; + isPositionValid = false; + } + } + + /** Iterator over values. */ + public class ValueIterator extends AbstractIterator { + public V next() { + findNext(); + + return unmapNullValue(values[position()]); + } + } + + /** Iterator over keys which supports access to unboxed keys. */ + public class KeyIterator extends AbstractIterator { + public Long next() { + return nextLong(); + } + + public long nextLong() { + findNext(); + + return keys[position()]; + } + } + + /** Iterator over entries which supports access to unboxed keys and values. */ + public class EntryIterator extends AbstractIterator> implements Entry { + public Entry next() { + findNext(); + if (shouldAvoidAllocation) { + return this; + } + + return allocateDuplicateEntry(); + } + + private Entry allocateDuplicateEntry() { + final long k = getLongKey(); + final V v = getValue(); + + return new Entry() { + public Long getKey() { + return k; + } + + public V getValue() { + return v; + } + + public V setValue(final V value) { + return Long2ObjectHashMap.this.put(k, value); + } + + public int hashCode() { + return Hashing.hashCode(getLongKey()) ^ (v != null ? v.hashCode() : 0); + } + + public boolean equals(final Object o) { + if (!(o instanceof Entry)) { + return false; + } + + final Entry e = (Entry) o; + + return (e.getKey() != null && e.getKey().equals(k)) + && ((e.getValue() == null && v == null) || e.getValue().equals(v)); + } + + public String toString() { + return k + "=" + v; + } + }; + } + + public Long getKey() { + return getLongKey(); + } + + public long getLongKey() { + return keys[position()]; + } + + public V getValue() { + return unmapNullValue(values[position()]); + } + + @SuppressWarnings("unchecked") + public V setValue(final V value) { + final V val = (V) mapNullValue(value); + requireNonNull(val, "value cannot be null"); + + if (!this.isPositionValid) { + throw new IllegalStateException(); + } + + final int pos = position(); + final Object oldValue = values[pos]; + values[pos] = val; + + return (V) oldValue; + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java new file mode 100644 index 000000000..da9c383fe --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java @@ -0,0 +1,689 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; + +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * Open-addressing with linear-probing expandable hash set. Allocation free in steady state use when + * expanded. + * + *

By storing elements as long primitives this significantly reduces memory consumption compared + * with Java's builtin HashSet<Long>. It implements Set<Long> + * for convenience, but calling functionality via those methods can add boxing overhead to your + * usage. + * + *

This class is not Threadsafe. + * + *

This HashSet caches its iterator object by default, so nested iteration is not supported. You + * can override this behaviour at construction by indicating that the iterator should not be cached. + * + * @see LongIterator + * @see Set + */ +public class LongHashSet extends AbstractSet implements Serializable { + /** The initial capacity used when none is specified in the constructor. */ + public static final int DEFAULT_INITIAL_CAPACITY = 8; + + static final long MISSING_VALUE = -1; + + private final boolean shouldAvoidAllocation; + private boolean containsMissingValue; + private final float loadFactor; + private int resizeThreshold; + // NB: excludes missing value + private int sizeOfArrayValues; + + private long[] values; + private LongIterator iterator; + + /** + * Construct a hash set with {@link #DEFAULT_INITIAL_CAPACITY}, {@link + * Hashing#DEFAULT_LOAD_FACTOR}, and iterator caching support. + */ + public LongHashSet() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * Construct a hash set with a proposed capacity, {@link Hashing#DEFAULT_LOAD_FACTOR}, and + * iterator caching support. + * + * @param proposedCapacity for the initial capacity of the set. + */ + public LongHashSet(final int proposedCapacity) { + this(proposedCapacity, Hashing.DEFAULT_LOAD_FACTOR, true); + } + + /** + * Construct a hash set with a proposed initial capacity, load factor, and iterator caching + * support. + * + * @param proposedCapacity for the initial capacity of the set. + * @param loadFactor to be used for resizing. + */ + public LongHashSet(final int proposedCapacity, final float loadFactor) { + this(proposedCapacity, loadFactor, true); + } + + /** + * Construct a hash set with a proposed initial capacity, load factor, and indicated iterator + * caching support. + * + * @param proposedCapacity for the initial capacity of the set. + * @param loadFactor to be used for resizing. + * @param shouldAvoidAllocation should the iterator be cached to avoid further allocation. + */ + public LongHashSet( + final int proposedCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { + validateLoadFactor(loadFactor); + + this.shouldAvoidAllocation = shouldAvoidAllocation; + this.loadFactor = loadFactor; + sizeOfArrayValues = 0; + final int capacity = + findNextPositivePowerOfTwo(Math.max(DEFAULT_INITIAL_CAPACITY, proposedCapacity)); + resizeThreshold = (int) (capacity * loadFactor); // @DoNotSub + values = new long[capacity]; + Arrays.fill(values, MISSING_VALUE); + } + + /** + * Get the load factor beyond which the set will increase size. + * + * @return load factor for when the set should increase size. + */ + public float loadFactor() { + return loadFactor; + } + + /** + * Get the total capacity for the set to which the load factor with be a fraction of. + * + * @return the total capacity for the set. + */ + public int capacity() { + return values.length; + } + + /** + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. + * + * @return the threshold when the map will resize. + */ + public int resizeThreshold() { + return resizeThreshold; + } + + /** {@inheritDoc} */ + public boolean add(final Long value) { + return add(value.longValue()); + } + + /** + * Primitive specialised overload of {this#add(Long)} + * + * @param value the value to add + * @return true if the collection has changed, false otherwise + * @throws IllegalArgumentException if value is missingValue + */ + public boolean add(final long value) { + if (value == MISSING_VALUE) { + final boolean previousContainsMissingValue = this.containsMissingValue; + containsMissingValue = true; + return !previousContainsMissingValue; + } + + final long[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(value, mask); + + while (values[index] != MISSING_VALUE) { + if (values[index] == value) { + return false; + } + + index = next(index, mask); + } + + values[index] = value; + sizeOfArrayValues++; + + if (sizeOfArrayValues > resizeThreshold) { + increaseCapacity(); + } + + return true; + } + + private void increaseCapacity() { + final int newCapacity = values.length * 2; + if (newCapacity < 0) { + throw new IllegalStateException("max capacity reached at size=" + size()); + } + + rehash(newCapacity); + } + + private void rehash(final int newCapacity) { + final int capacity = newCapacity; + final int mask = newCapacity - 1; + resizeThreshold = (int) (newCapacity * loadFactor); // @DoNotSub + + final long[] tempValues = new long[capacity]; + Arrays.fill(tempValues, MISSING_VALUE); + + for (final long value : values) { + if (value != MISSING_VALUE) { + int newHash = Hashing.hash(value, mask); + while (tempValues[newHash] != MISSING_VALUE) { + newHash = ++newHash & mask; + } + + tempValues[newHash] = value; + } + } + + values = tempValues; + } + + /** {@inheritDoc} */ + public boolean remove(final Object value) { + return value instanceof Long && remove(((Long) value).longValue()); + } + + /** + * An long specialised version of {this#remove(Object)}. + * + * @param value the value to remove + * @return true if the value was present, false otherwise + */ + public boolean remove(final long value) { + if (value == MISSING_VALUE) { + final boolean previousContainsMissingValue = this.containsMissingValue; + containsMissingValue = false; + return previousContainsMissingValue; + } + + final long[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(value, mask); + + while (values[index] != MISSING_VALUE) { + if (values[index] == value) { + values[index] = MISSING_VALUE; + compactChain(index); + sizeOfArrayValues--; + return true; + } + + index = next(index, mask); + } + + return false; + } + + private static int next(final int index, final int mask) { + return (index + 1) & mask; + } + + @SuppressWarnings("FinalParameters") + void compactChain(int deleteIndex) { + final long[] values = this.values; + final int mask = values.length - 1; + + int index = deleteIndex; + while (true) { + index = next(index, mask); + if (values[index] == MISSING_VALUE) { + return; + } + + final int hash = Hashing.hash(values[index], mask); + + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) + || (hash <= deleteIndex && deleteIndex <= index)) { + values[deleteIndex] = values[index]; + + values[index] = MISSING_VALUE; + deleteIndex = index; + } + } + } + + /** + * Compact the backing arrays by rehashing with a capacity just larger than current size and + * giving consideration to the load factor. + */ + public void compact() { + final int idealCapacity = (int) Math.round(size() * (1.0 / loadFactor)); + rehash(findNextPositivePowerOfTwo(Math.max(DEFAULT_INITIAL_CAPACITY, idealCapacity))); + } + + /** {@inheritDoc} */ + public boolean contains(final Object value) { + return value instanceof Long && contains(((Long) value).longValue()); + } + + /** + * Contains method that does not box values. + * + * @param value to be check for if the set contains it. + * @return true if the value is contained in the set otherwise false. + * @see Collection#contains(Object) + */ + public boolean contains(final long value) { + if (value == MISSING_VALUE) { + return containsMissingValue; + } + + final long[] values = this.values; + final int mask = values.length - 1; + int index = Hashing.hash(value, mask); + + while (values[index] != MISSING_VALUE) { + if (values[index] == value) { + return true; + } + + index = next(index, mask); + } + + return false; + } + + /** {@inheritDoc} */ + public int size() { + return sizeOfArrayValues + (containsMissingValue ? 1 : 0); + } + + /** {@inheritDoc} */ + public boolean isEmpty() { + return size() == 0; + } + + /** {@inheritDoc} */ + public void clear() { + if (size() > 0) { + Arrays.fill(values, MISSING_VALUE); + sizeOfArrayValues = 0; + containsMissingValue = false; + } + } + + /** {@inheritDoc} */ + public boolean addAll(final Collection coll) { + boolean added = false; + + for (final Long value : coll) { + added |= add(value); + } + + return added; + } + + /** + * Alias for {@link #addAll(Collection)} for the specialized case when adding another LongHashSet, + * avoids boxing and allocations + * + * @param coll containing the values to be added. + * @return {@code true} if this set changed as a result of the call + */ + public boolean addAll(final LongHashSet coll) { + boolean acc = false; + + for (final long value : coll.values) { + if (value != MISSING_VALUE) { + acc |= add(value); + } + } + + if (coll.containsMissingValue) { + acc |= add(MISSING_VALUE); + } + + return acc; + } + + /** + * LongHashSet specialised variant of {this#containsAll(Collection)}. + * + * @param other long hash set to compare against. + * @return true if every element in other is in this. + */ + public boolean containsAll(final LongHashSet other) { + for (final long value : other.values) { + if (value != MISSING_VALUE && !contains(value)) { + return false; + } + } + + return !other.containsMissingValue || this.containsMissingValue; + } + + /** + * Fast Path set difference for comparison with another LongHashSet. + * + *

Note: garbage free in the identical case, allocates otherwise. + * + * @param other the other set to subtract + * @return null if identical, otherwise the set of differences + */ + public LongHashSet difference(final LongHashSet other) { + LongHashSet difference = null; + + for (final long value : values) { + if (value != MISSING_VALUE && !other.contains(value)) { + if (difference == null) { + difference = new LongHashSet(); + } + + difference.add(value); + } + } + + if (other.containsMissingValue && !this.containsMissingValue) { + if (difference == null) { + difference = new LongHashSet(); + } + + difference.add(MISSING_VALUE); + } + + return difference; + } + + /** {@inheritDoc} */ + public boolean removeAll(final Collection coll) { + boolean removed = false; + + for (final Object value : coll) { + removed |= remove(value); + } + + return removed; + } + + /** + * Alias for {@link #removeAll(Collection)} for the specialized case when removing another + * LongHashSet, avoids boxing and allocations + * + * @param coll containing the values to be removed. + * @return {@code true} if this set changed as a result of the call + */ + public boolean removeAll(final LongHashSet coll) { + boolean acc = false; + + for (final long value : coll.values) { + if (value != MISSING_VALUE) { + acc |= remove(value); + } + } + + if (coll.containsMissingValue) { + acc |= remove(MISSING_VALUE); + } + + return acc; + } + + /** {@inheritDoc} */ + public LongIterator iterator() { + LongIterator iterator = this.iterator; + if (null == iterator) { + iterator = new LongIterator(); + if (shouldAvoidAllocation) { + this.iterator = iterator; + } + } + + return iterator.reset(); + } + + public void copy(final LongHashSet that) { + if (this.values.length != that.values.length) { + throw new IllegalArgumentException("cannot copy object: masks not equal"); + } + + System.arraycopy(that.values, 0, this.values, 0, this.values.length); + this.sizeOfArrayValues = that.sizeOfArrayValues; + this.containsMissingValue = that.containsMissingValue; + } + + /** {@inheritDoc} */ + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append('{'); + + for (final long value : values) { + if (value != MISSING_VALUE) { + sb.append(value).append(", "); + } + } + + if (containsMissingValue) { + sb.append(MISSING_VALUE).append(", "); + } + + if (sb.length() > 1) { + sb.setLength(sb.length() - 2); + } + + sb.append('}'); + + return sb.toString(); + } + + /** {@inheritDoc} */ + @SuppressWarnings("unchecked") + public T[] toArray(final T[] a) { + final Class componentType = a.getClass().getComponentType(); + if (!componentType.isAssignableFrom(Long.class)) { + throw new ArrayStoreException("cannot store Longs in array of type " + componentType); + } + + final int size = size(); + final T[] arrayCopy = a.length >= size ? a : (T[]) Array.newInstance(componentType, size); + copyValues(arrayCopy); + + return arrayCopy; + } + + /** {@inheritDoc} */ + public Object[] toArray() { + final Object[] arrayCopy = new Object[size()]; + copyValues(arrayCopy); + + return arrayCopy; + } + + private void copyValues(final Object[] arrayCopy) { + int i = 0; + final long[] values = this.values; + for (final long value : values) { + if (MISSING_VALUE != value) { + arrayCopy[i++] = value; + } + } + + if (containsMissingValue) { + arrayCopy[sizeOfArrayValues] = MISSING_VALUE; + } + } + + /** {@inheritDoc} */ + public boolean equals(final Object other) { + if (other == this) { + return true; + } + + if (other instanceof LongHashSet) { + final LongHashSet otherSet = (LongHashSet) other; + + return otherSet.containsMissingValue == containsMissingValue + && otherSet.sizeOfArrayValues == sizeOfArrayValues + && containsAll(otherSet); + } + + if (!(other instanceof Set)) { + return false; + } + + final Set c = (Set) other; + if (c.size() != size()) { + return false; + } + + try { + return containsAll(c); + } catch (final ClassCastException | NullPointerException ignore) { + return false; + } + } + + /** {@inheritDoc} */ + public int hashCode() { + int hashCode = 0; + for (final long value : values) { + if (value != MISSING_VALUE) { + hashCode += Hashing.hash(value); + } + } + + if (containsMissingValue) { + hashCode += Hashing.hash(MISSING_VALUE); + } + + return hashCode; + } + + /** Iterator which supports unboxed access to values. */ + public final class LongIterator implements Iterator, Serializable { + private int remaining; + private int positionCounter; + private int stopCounter; + private boolean isPositionValid = false; + + LongIterator reset() { + remaining = size(); + + final long[] values = LongHashSet.this.values; + final int length = values.length; + int i = length; + + if (values[length - 1] != LongHashSet.MISSING_VALUE) { + for (i = 0; i < length; i++) { + if (values[i] == LongHashSet.MISSING_VALUE) { + break; + } + } + } + + stopCounter = i; + positionCounter = i + length; + isPositionValid = false; + + return this; + } + + public boolean hasNext() { + return remaining > 0; + } + + public int remaining() { + return remaining; + } + + public Long next() { + return nextValue(); + } + + /** + * Strongly typed alternative of {@link Iterator#next()} to avoid boxing. + * + * @return the next long value. + */ + public long nextValue() { + if (remaining == 1 && containsMissingValue) { + remaining = 0; + isPositionValid = true; + + return LongHashSet.MISSING_VALUE; + } + + findNext(); + + final long[] values = LongHashSet.this.values; + + return values[position(values)]; + } + + public void remove() { + if (isPositionValid) { + if (0 == remaining && containsMissingValue) { + containsMissingValue = false; + } else { + final long[] values = LongHashSet.this.values; + final int position = position(values); + values[position] = MISSING_VALUE; + --sizeOfArrayValues; + + compactChain(position); + } + + isPositionValid = false; + } else { + throw new IllegalStateException(); + } + } + + private void findNext() { + final long[] values = LongHashSet.this.values; + final int mask = values.length - 1; + isPositionValid = true; + + for (int i = positionCounter - 1; i >= stopCounter; i--) { + final int index = i & mask; + if (values[index] != LongHashSet.MISSING_VALUE) { + positionCounter = i; + --remaining; + return; + } + } + + isPositionValid = false; + throw new NoSuchElementException(); + } + + private int position(final long[] values) { + return positionCounter & (values.length - 1); + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java new file mode 100644 index 000000000..503ac6af8 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.collections; + +/** This is an (long, long) primitive specialisation of a BiConsumer */ +@FunctionalInterface +public interface LongLongConsumer { + /** + * Accept two values that comes as a tuple of longs. + * + * @param valueOne for the tuple. + * @param valueTwo for the tuple. + */ + void accept(long valueOne, long valueTwo); +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java new file mode 100644 index 000000000..aa711216b --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java @@ -0,0 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +/** + * Copied from + * https://github.com/real-logic/agrona/tree/master/agrona/src/main/java/org/agrona/collections, + * which is under Apache License 2.0. + * + *

We can't use agrona as a regular dependency as it's compiled for Java 8 and we still support + * Java 7. That's why the relevant classes are copied over and methods referencing Java 8 types are + * removed. + */ +package co.elastic.apm.agent.profiler.collections; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/package-info.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/package-info.java new file mode 100644 index 000000000..8e87ae790 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +@NonnullApi +package co.elastic.apm.agent.profiler; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so b/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so new file mode 100755 index 0000000000000000000000000000000000000000..cbbfad6606d5cbfd094ca92321b69bab2e69f4c6 GIT binary patch literal 262320 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 HdUXCjmuIlo literal 0 HcmV?d00001 diff --git a/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so b/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so new file mode 100755 index 0000000000000000000000000000000000000000..1ddf056895ede560de15da3b1faad2a0c75b1c93 GIT binary patch literal 232852 zcmce<4SZGAmG{5zOCW>@7fIVR(0Wl?qeZ=7)L3b~2viWP;YCGDO=1!y8p#!+MoXVw zAiOC`fPm3bxvg|+9cQG~w$#x!Ryv}k6)SeAW!n4VDKXHNPVEfSahm`4ch0`yZ~?Xb zJpbqFX6>_I)?Rz9wbx$z<(zzJ>dems0s)i%g-oT@V#2o;o{Z=;Ood>Bi*LOySn)7Hr2aX;0M5N4C_Z+|x#3aT&K152*O+}hXtlR3_aX(PznMkuyn#GZJ>tWBEAp1}HOaIT4(WY6lc z@%xPl21?6{C#mc=2#*mSC;SuPNy4`XPZ7RD(C@p1?-6zozE9BaIS-h#hY$U|d9tea zs!Lya>fmdC7`Eg7KMuQV?!>P7|NO!Kc;p9A=aJx6QvHt<w$bWQjKG*Bd(t_vf3h>Vt68lqouN2^$E(Nqd<$q6#Ykzuvv7r3H0{j6Vy((Yj ziOR*G=PwlSZ(#v{78bPkctLyLbTN7V2;UD1@^2~N|E7Zc&H{P;yyPfC;d`>6{^Ek? zSq0DkS%AO7*FUlPqJr`>HOTEx?aeKa$IlD&`GtI8pvs?Fz|Tbm<^Nhh&lv^%`*Z=n zukq0%Qf3wK|4Rk*Kj@RM$`=>3e|JIq=N8cWi-P>E1VKp_UtTVd-}43iJGFrSPZ!Yp<%0Yb1x-9dLJm@&u*V73(9}c*M5}v6$SMF)R!-Q4l2Mmy5M=SZ~UlyZvnqwFVNRt70~nj z0)F`(O%!}fd8wMO%FNYOmC%kIJ9s zdshC?g7%kF|M19wsYb4+o1rE=694>?iIkN8U2Y~}Upikf1NmZC>T{kFoN7wVJK)dz zo2TM${+UMh{K7Lzs-J_1-s^&QfKW9ZrYi-1``dwRaKpnUe2_C919pr`BUfSC^;RQ@^eZ3my$Q+i$} ze$oGO+uk?9KmV10NukHe{{;3l^`U^7X#00H_GbPbW42g+h6?EaF7@AG(%5I|`!%fE z^qnsx%Q5QcQr30HIb-IEE+#h>el9#mVt*`TALRRRA_E&zSAtM zU)Z|1rKu?~kbDfDn5n(~vvb<2n_Cyx-dMl5scQPd=C;Y>r`|bG91_}_+fGz$id*dZ zxz~Jle5{tj4 zW*S=--+i08>8?f1FmQ1j!dm2JEueK5Z<9FL(p10j4l(BD_V(ID)1t+#5>DIQwRggn zJl9($G@8W=kD_mBg2UI(X&ZlGZEY(OYpTDE9xiTL)Hc0=)bR}q>KE0vEvj#BTMUg; zXOnh5oNaE%m!3Yic5bY8>YNK}Yi7(jzZQK$04-tEIUr|n{oP?X_>eYrp! zTQn8S6KbcUFoZT~=xt56Azg@CSbvMTeR1m|#T)Buyyn`fYiHjGN1B?ZH_vISO4K(m znm(f$rIXa#+sBU|Kf!AjW;QRp`9v+xsTV7!s^wVioTiqhhPLsG+osQ{hK+4iElrCT zPj8+|Atg+$h8InX(B-K$;(fmH+J&u+O>PMh*K#~@?E}QMgRG#X2}fIMcUvOiJzAoK zK|5MnT2W7WZ*KGA66*hJo=lJu-Q3h>6{&Usg9U@qeES_uwTafnba%p0Mm4l9Tuez! zTYX#WB9~>e=Zu%q!Ic4FVgWHww>2$n6Pu4Fi_NYnDJhTv@xQDbZ5j z&@}ayTWV%B+(N?(@6x~Y>f!Bjn&`h^WmH=cy_IkDQ=bKFt(w`-|5 z?OE=rvv0XYW1}H)w+k@sE(?HN2}_+aZ=*FfbsoZa3a>6Jv5b+U#m>-kROi+|uM_ z9dGo!2}FASeo9xhCa~dfK*e3dauo@;p+{;Fi;uaHM!_pJiYd6Ld2y{<$y%8A(+b9` z6`PQ+QHat`W{`X#1Gc`U>1e_LC#MW>a(tCXPwP$I3~(Z&HT5Wl_e0;nZ2F#N7_Q8n z3s>F`4Jv!h-&od8SMMg^JlYc|%JJGjVN7d-=3p<@dIuB4!2F7rf~++?H!>Lm7Bw(U z0~g8}DzKJ+hlS7-;Y~VoE~q`;vis%`^8bceq-Aka)2+~_xkT0m(gv*H^#5mcPN+K8 zvipqs>^T=9$1M-STl@LVZJ9bE~{;`KLGEQNO6Mw!Y!^JDL}{XcEnh$7C(STfrAG&8-b>EtUd2 zSy}yst+k@r#Rh5aJYaA`nGVctx78=iO$`gFBU*4FZo6Zl(rRnl<#f43wl2KMxV=^% zYHj~+^%`k3SJdBGU(3RT)k3Sr@}k;Fi|=08P?LY6&vWW;OW-}dXHj?k6MG-US`+VG z3R<*ka@RQTUAn2gso{>cCLD5%o?myZui6Ud@EXOx`rA%qhOO_Y`q%v5tbf$>(fmm! z!lP}|vYl6s&W!`Z@NesiJ5e+uPw7cxi?q~Cx*0#L_Ns+5Tk9JK9wxQ5)#F55@!9G5 zocziX(JxLkH8kJU+`w>hSBLqm+hBHkeOucicO@~fusoW~DA4-r*qjp-mTuo#Q0k6_ zZlX7<4|M;1y8l(Iog3vQ;8p#77%pmPy)99{$oh$YClZegyv#+olEU$K+S7!ri`yNu zsPzsP75-{f-Zx~9tZluimPT%Fs=ev9Hu`-Jtx<&awRIm|lj-*#y^Vs#33s&IV;Y*8 zWoO(a8}4Mo0u~@xf;QGp#+|w@tdgZzs7Y6NFG49=nit|;VZ#env7R3Lma=SoxZ}+( zWiP6oeR4K1VKA}I#etlvxr3T*t#`PK`J;YG^ZW3l{65dSmZv*@pXa^GLixayHN9rW zow&NVV71fPSYXHE?&)%U^Q%lZ&N@?_dG~eZfWLe`1I9&b7pi^~M;*#{Ev0>ymgXB9 zTh3kFdhP^8?P%3=OwWtj6dkW9yB!Um_=FxW)Z=Xfv6~wjY8MX_Yw*%*n;Pre>d})M z7ccg9XmysU3^U5#lEa#GA0aEnl$@L7wP%_d^YafUy$?mAomkOgd*-k zgxu%B+(PiugX9h&c=W(Qa4Sl=t=)7r-U{UjPf-f*9q+~^Kt46=hg1>Aakw{U(a#)ihTG2X7h3R;bZdm z9hV>S=LuYKHu)vA7WP7=SQEMj(`T6&GxzNtPMN|%{O?~Pxpd81gy4jqJ?n_ zr&u`6!kHGPvoL01m4($7PP8y?;S>v}Svb?e8Vl!HIN!oX3l~_}VqwC#ln<@ z+bn#{!tEA5ZQ%|JpS7^Z!d({bws4PydoA2&VcNn&79O_nZ41q79$&&17F$?qVZ_2R z3rAZRwQ#J3<1CC>SY=_gg%d4|TR6qS8Vl!IIM2fQ7S>s~z`_;_6Bf2v*luCBg((ZS zS@@WRPguC!!ly0VVd1kD_E@;f!owDp@Adct6Yoh4U;-TKJfS zdn`O+;WX|*$X?V~IM2d53l~_Ju&~|2q=logJHdMHJ6hOdp2tLFe1q&au@Cge$RBtK69u`$5f|jH&J;lw2Ga!b(`O3e z8`lVyAwR(n7&A|h%S`hHxh!5Mh=McmWaBY(j~$X{?V z@)x`V`3v5O`~~kq{(|kuU+^B}FZc!IFSrEx3w{y#3;rMEFZd3ob+cg3FP= zUpwFZea+iJ$XUN5~v3 zPCnNgPM_n1(_5Wk>2>Mi!#{AUw}iNOye@n3%WJdM?>y_|<1t^n(ie~V;^n@0#1}92 z#f>k1I{e4j7A#~0t_i}(2AJACo&zW8Ilc*+;=_QgAW@zuU~(id;{ z#S^~x0$;q&7oX>g*ZAVoeDSz1UhRv=eDO+OJnD;=`{EH_yx12vzW9;9`1tRO_xj># zUwof0zQ-5e<%{?D;yZlt?Y{V9zIe(P@Akzzeeu=4c+wYd_r(*w_yS+N&KIBOi`V$# z(|qx`FJA47$9(ZhUp(rIm;2%oU%c2CH@^6h|Mv0U7w`4O)4upVUwn@*zRMTy@x^!e z;@f@k$9(aWFW&8oclzS1eet9(-tLPheDMXoc%3gk&lj)p#i#k=abLXJ7mxYkmA-h? z7cck4BffaCFK&GCBS(Dv_r-gC@w6|#&llh0i|_KqdwlU7zW8=u{4rlV<%@Uw;+?+u zYF|9*i?{pY3157HFJ9-1&-2A=eDP_%c-$AS_Qhkqc%?5M^~KA5@rW;8?28*;{K$X# z`0tDN`r>I{e4j7A#~0t_i}(2AJBY`oo%2?8e5+Hs{r+q?&Kwu-$c9ZSv!=iQpkTCS zs}qfFb&6`%rAJ4%I-$s>1HtIJbm^WM+1Rw_dTamM-&e_;SiLD^x|kYwcZWo9eI;d`4x_{wkvc+|u$@b3L>I{O``QT4a z+Ulgo6UKNnt512FRp$J^dj0yrt>)lp(~&JtbU6P)ygJ&Et%-GH8%e7sZ5pA5P)C?Z zh(|k|DTKMi=fygl`Ggm#_m@{vx%LsXH!PS7MAoGKq1k-oDIv8+e594#DsoCKMA&2@ygWp`0+95G9NyEWiCnP6u!`p$%LMfGvcCo(aq7 zlZmHuZ})6<-i{#49_V;!ZMG}0)(J;bnZdDCrefIItmx~WwAR^j`&#GH%gw>v$bE1m zl@X6JLprj-my_9eG?lwzcrv?#GP?*h;C>AGKjxFY^dj@W{Rg{A>n1N4-E<%nLwBOf zvTvWa+&R4Zx!w+N3-8=WmopQdRw0{-#H)yZ1DszWG*VafG8Zq0){PFHcMk8ck#$Zj_@N?>rnbZG^@%gJueFdk`OXbI3%= z?>%^lIrLD->>@ltDAdhp+|&1;(&qco_ecM?zi;CITw&x-V0|H{mqXd;ragi z#J3K7rQkcV-ov-n;`5Kmh^2Ac`^0$yW!g=O$ z^T53qnq(_vze17C2Z}TvC^MdZnOG_(+GjkF%0)U;xjmOo&c%b_+>wv%%mfdTM}D9u zmFt2pU6HNM;T5Du*QF!af#D{e(|AgfhaGXs(;dzQJv*J5kq*aT8^VNQLMfq)5G9Ny zR1(G!VuY&LPNy0;kq{?LAxtCGksgFr>5=FijjcUAgS7}eNdNaGW(hnfr~hL8p;1Az zcYM&a5iiv3cB?lZe4ji%^iF@@cJQ~X^KhGup5D~KP8~ShalYuQ@%{RW?uUy zyx0akTOMF;vtzvryCNMgqrcMeV07aFzH*R5zXKJT2PwM*IaE=m%6MgD!$Ps;SreO_ z9%Sa^G?%I_b=H@ICm0isIWCTojFY_xbr0dt{n#^bRY$fu)!>q@373gXN^7i68j;Ef z&sR@dntndAJtG=Ugl{#tWG}aYFA2W9ygd5B7kdpm>$X3l8oUwKzM-%5&`%BdB`Sd>$f3Ej=+4+^=S&N>7Q?^BR#cxlf8)fnJ&i*>JF*ld@3E0+p#?+)T z5wqUe8S(g*g!agJt(hIr^R?4GzNwDrXS=Rs8|m99>-C8-53ef>HZ8u~`NK?ea4oc& zNGhkX{}IyrND~j*sTYp^*a<~9A1F2}vPI0ZDpxPt!dzBDU*}OK6x$3xH@Z5Wyl_Y^ z86TeOj>X|cP3|w3#dD!}YQ(?O=NLQ_zcdyqeeo{%P*u)*bMUp2@Fq-5^!N84MnGx8 z_74TkYJ$eUi9CboMYFikRz*O_!7R-NvEgE*~jdzURv9A!t1m zw|qb^G*3u3g79-VeNmtHs6X+p1EwdH+eN%MdcVuJaCD<1{IXd;x)j)RzhemhRBR5q zV~ReN)y>F;qaSx|M?A>5OmsT3#k_az@(WRyBrN_QZ8`Ad+StJZA!raE!->nYw7sgx zn1@yGK3lKY()LTri#FG%A^$I75AUD)`z!vFJlUlMw$5)!`y**l#`cu^LZ*(!4>PAF z;9&%Ql*MOcOW{e_jCItfGS#c;blU5%bv(CNJhD9+Cqcs;g6&Aq&#NuX;hL%0LC~u5 zb+$a))D;0=S&e1!Eo3$y_?e~mKcMZ;(57}L`sh6@dhhjUEjF`q@Zk*uy@Ml5nP>0M zK95aUj+_lNXwH>BeD$he?&XQ8+>R+za-CCtobEX|(jp>IknVj1gY)HF$LO-Rq zWoAaUEIJfBJ=A&SfBFx)dIGM)SDo`#6#Qc;H_;4r{$++a_|?4Q*0O9MnaVUKe(td0?`UlP5A{tvmAMx9c4$Us5dGg#HYFF2 z40Vc$4^E{r509Fho5J(N*eK>w&qiGipN+XL7jfbBIkqTEs>u)FKQ2K3%4xS48>4o^ zj5p-+#$bHXAHkPT!I!;f%*dGNhIG%RkEY)yuDYf!m1|5c&z2;lyQU95Fh0rG zKByRMuE~uw=4BJ>a&@6Jw$YKT;!W7x*)AWs=8l{ao)-3VpC~qu*e~KC9Y`pQs^b5%Aa^%%Hp&>m4+QuQH zO5`&R{@#R){QUWz+wR<)#!;Tuu%$Jsd2QWuz4`QFo1UlP*U-pHFQ;{^LBcpqj^L7)5bhDZ14-nLUwq@VD_`o zfe1J|m`}P_czhAge7j{a@@c1(x*_lcp`i@EM+l{aVnWYfSSNu;wB_dx)hUZH-^Mk6 z!5`)g#>F$tb)m1e26NkMwmRD*S}Vpw@)PBcX^tHQeiK{aaF%mWHtMxg!pzB=oE?o* zavN@XG5w=g(EXpac(!U>L;6NDDYwx)pH`nHo!{W%85^0A6`fxuV$50Cg#28TpI`IOqa_!!E}efag+F_k7oyCwGVmRdpeZNhcfB4Q=QSaKV?AK zV#zU-V2FUEG_A4JY_hL}{Q+GvjG zbmNm1$A^^7!}jDD8!s@2zSjSW!6wbc4ktMUs>yvES*Ie5+1M4?K)U+?HoGrqu2w$$ ze$**9=Io%k7QSA$^aISxA#)&38AFyM@pd5ZgraBVOEgZfg8;EqI zwRaHwL@HO6GzSNv&w1Z)^eUf^32xbgV{qRh+?$=^^sij}rD`{QKeqRN=e^qca6wx= ztf4l(Sd!ax@vz*sf1R1z5-@4`a<5)8HTQgNDzmXDnqEJ71@p$NoW^HSO)~uC84OO(I)A{Jm>7vM)>5cVc(w(#sG&f|5YL=#Z<)bj)Z@dV)&Kj1Jum1I$Q<*{WvFU9W z&dhBLhth*i~ zc_qj9IP%}QDV~c&mOEuN%k{iZbL0ZbFU>rVUOOI})w4DmiY!Z)Vp~FHeQzlCKsscW z_cFFCR9`$7p985>X48i)?@L1IS4U0Fy$s*i)31k@u1SmM#$2D1ju~qwhIB4x?P=b? zo@VYv4wZ3i9{dVp|B4ysTY};AkA!3D*z|@m%hRD)NBU9mFM4`fd4pqTAX6`|m^9@p zeTbWWMtTG78hE32B#X*8ekSyO^GxQOWTmUOl8dV^#ii*`=UJXCUK*F~UbZGZq;qZh zo5;dYw!NqSx5_Vt|4Z3lE`tAo8uY*T>KuN4Nc>qkY(s|4C`g{e8!kr%h~#J8!$XvhFGR(wz=;eluOz-d^*QjLHm(ccjNm z@5mmGcVxGo_jE>Uh(PSo^oCcqLEn6rz9QOH*#X;MlgcSA{~Rkm=dI^2MR%g9%vYx| zmOh034;hwQetAbGjGVT8aAt0OFpz$99(#Vs>eZoBbDbl{q&Gw*ZsPB%ipG?hdK@YoTdgE)`f>dV3 z%_}n+hix_ezb%(NfQKvUpht3r*MZ_W_-m<9k(rY@yym&$<>ZCZE7K2TQkh{r$@Egr z9Nvj<;cR2AJ3kIZw%TX;LteI5JqW+%;tQFI_3Ks+o;W?@s#t)pX}8UmM`N%H`#B7#g=;g*@Y3=|Iib(_IDn1I@0_`b5Ip z&y@WrX6$Kx56~B6ldH#$3{J?Grq73?E*)D>>!LmU0O;QEitLFebG6Mny~0HAcN#b| zDMGed%O>LHpz^j}iXDng&Tc$Ub!O+-=W=yChF?`wvl6*Jc)&0x2Jtma-SP|MZyim$ znthz*EAsy6%zKXd3^8l_w04)>63xfZ`-T^HvoCraZj9c0;&#{2)1K>1#@TbSZD?GVa+a-e?XHoqOTB+7AHNyCG54lYA@&U` zv=1!*++SX2p*jcEUQDn~{$MKibYiRXG`7K;&mKG=KN;CL(Ih-VcLLFPuADOz?YV~F zfy-s)jpnb_bAiD|E5QaT#Azc(CBWiAqr z8S~8j1N9keZw#WGe6#TBn+_B+=EBjC(-ByC!0W#|_9~R`o^{^$Toq^7JbNVLc^ySRyUY-1$ext9w)%o_KzV=R2d#>(R zY#6S(H3M_9zr81Hd+`5kXz=`xOOI`H1oF^bh%L74KE{jIQ4Pa8vfui&XLDcEel#{O zY*w@1wAy($+Om!#eiTWN+qEF2??z_F83IwBIV(X)mWdzgJH?ygE=3>y&M| zE_WgLE2$SYE1X#8P^U7XbEB>UMd;)d=qrw@{zfN`Ez+6GIL<`|#Wau4$xUwVnUdJA?bSu9uEfA5t<0>1@<9>zFOh|0^_Ss z{V%5f!=QET1xNcI>bx$^USP?Rk8O39kgoGCcW)WFnAAq6B(*GE5_^cbV@~e7)RheK zHeEO);Or)SiVvsC?gnR7K^l%h$ynM^8>~6u*X$fd?U`WgH5dPnz*GDIZ{A%2&9dVf z%ZKdw^Fxds&UM_re3~}C*WdrL_E^i3-X803v`3C?G>^=L*Ek0Q&(nBzcoxmtdksK$ z4f~kPeHGI<=izKTTZ$c=OF7kljj~fn_sh$~ljtIM?vR_xyi6IdF7_anbR$}i3 zS2)pI&2N>g!q|<8aN!ypI zDCYy*z0lhAcE-N?6ZB+I5{{3|w%Gp6O$>FyDb~2~(Vdf0_ht`(Bg|O5kMSv-4_lm~ zD?DQ3fggxA=;yq)MEUnApK+!5I>q6e;^8ser--g{?j`Lw9;J@u(LI!zseZyA)%_yR z;#vMV$#ac;&hzi*>|f1IjN)F+DCYXf=@$dZbiM5Jsh(~G7@L|4WFMCftD}$X#ni7% zZ@ucfnILQBkk-n>{{dU-%}J-=uc)kx&rDAD1h5&@39~1rdtG^b?sD;?o9un#8>XkB zAHk?}e|9d89K(^3j`T|Oi)W$8ngh36o~e(LX^=K=0;=2qZ{pr~m+r_Xarr?XN@8mc z2v;b&=79PhqK{>^kJ!7uQqF(fv1siSk7!_KzLB+uD7jk$A?(*(*7@ZQ?m{J7mu-hTW(zYW@mzPoZT4;`3*eAF+| zc|Yk-`^pBRod*Ju%d&sPhg`{g6dbcWU4rbw(Nmpi5ymU=B=|e*+F>s7B{n{bc$`vURsm)|Olec54uDl;x3n={mDpKK25tlC64)5O+m z+yq`)kxjBkykimf5z;f={iLnu{UBqK>>GAIn(n^ppW)eD*Z-3ruR6n%!R*`1IcK-y zG;_u3>?gH0n6x6TwU@>esOE_pGJD9I+Y_fx^^dc>e6(AHdcY_JHnG# zmDZkx{Ek5E^6bbhR@)%d09F!nK!Se4z6EVVW&hUUZ3pJt}id32fRW+xok>}i$u z`X7!hb7|X6zWO45Nyg$&p4W@zJHVswQ0ASRxf=yXQvVS!`LQsUu^^v6ctuBc7rti4 zqw*bDo7O>VjkoqHNv%u6!;+Tsw>rDW;CIu0$jr`Y9XWsFR%b}(I?fq3Il9jxzfk>_ zk6;ot*wSae`wXv+FO2o4kWcTd)4yR{)hqItl^Jps5 z^B?^OE5WIJ^+)|!L_cJkcfqsA>2K*tc(I+lJ&dQr%o(b?06oj|A_9)b;Ke@j)u!lF z8*2MEE{`@jJ=8l9k3Qz-QO4tu?x_qnKd?N?!$*1t!=ohcY$Vrv@(R)?@vM!sZ^w3X z?zAp_Mm73{EKO{;vm06xvb(WVrlWJcBVLJTE!dcgEx+*VOIqklLi`%z@heCf&FxX} zo=7ilgBLp)H{pQCi^=#M0c^Y0Ali5R7JP10zqzOKX6II?1HW9f1$&t1p+)WGWtW#5 z>&p_!tOuH(g>NBf=oAk6w@oLwM)FSqvxMgq;c%|MzYN$(;MEBefA$j7M)_)EeSTS+=6{*Y6wk1)5D=4QoU0*uakr z=V`JbdjehXwT8QgTDx7FW52AT4Ie^xX-v;1S4OH+j;y1|R`+4FzdV2P7FXsP`%!#+ zjsN^v$wVWYv@Xq=QePFgs-Q>rSMoaj1Ts{cdl(n&FO;y~(Klaxs>9v^*V50(*}UHk z@6yEcwr?hV#XjeDHEeZuVcT@~)4khEA2r?@Q|zm;o(gn&b}fLt(wU_CpP9aDz-FrL z#+p$@_z^5!=j61%yQIKYvhP^Zf^C&8mF*LrV{B#8vz5~Eqqg$8<7{Oy^*md-iZe^^ zOw!%w1Ebe4=BhT*Lex)S@02e6*v+_Vi40{8=-Ey6dja~A zAA_=;vTGWL!jrcLx-)qDA3fVC9_R0BpJ8=Gw(}q8@5yZE6XYGWoyYRxdGKz77qXpd zTXd>jwePo|&z{JB!YA&syL@8z{e*lP4xeN<&!|jgWFt*U z#W>%`cop9ZZS6JiPiul_WeXykTpI8NGTV8U%@j?cD0E;OG!KYA(N!pW_D3s{)H#t1 zu%~@v8TTp1`hF&~%HLrd4>A6Cc(!xO?*k^za|rk2cSxH^ zh^QXniN1gt{qOz#`$*gMolDGiU<+a2Ti^n^d-d4M^1AiT*q-%HWqQ4%z3UL?z@_Gf zKp>7E%KnCY)Uq0QUw2J9z<#vurC=-GpiD+~RdzSCCaJxV3hYIn=4JK&7oYO>fDQFc z9d|fm4>}?4wvFY!ZiM)P5#8{vBfWeCcX_a9sxxFFb*RsJ#Kke#?)$oDclSMq>*n5B zE|E`_$HzVF$5p=&r|<>2Gx7E%qT`yJ_D6(U=}%hPxFe0dXMd7?XRUn&b^b-)GjB({ zGceifFnJO5KsNIX`m>)rr75q(rfXkymoGi+EB~}F{jY~TJeq4#zVstDopm?oI;qUL z%=c$m9PIy<0Xerebcs4s)9NnD2w)abMrvd&utC?K*HNdf@ky zZic6tf4i>>nX!zOxdg2x(zG&~aj>U1;QA(GiOZK@F3iZEK+iJ=<{R#~ZgTW4ujWVf zO@6^%Y>|I|=L-6=b0E#@UoqvLrk?6Z-+l=$$@DO^r))asynWr+#0z4-z#bmmTg8Tq z48%fEI43t9Jnebharu68b6fC0KSBCLU;5{5I`<7q zDt+nK+Vs;&FY~3(vgw>x4(R4Go2K_54q16fH;wjbSyvb1le+g%IA?VCXX2^MztCsh z&lX>H5Z^k@(_5`$w_6<2QO0H8Hv6o1ZnxNYUZ*ww*V;7cu>1t+Sdui!rce)@KlC4D zj_hl+b)@Hed6sMzA)7Cg|LnY@I?{q(>>}iKC9PeR1K4B*;`<5l&#@Q8^E=n`o*bcc z`z2;KP`>VH(z}UEr!vjS?DD`boVqA`fI91o;(MZF=t8khA3DbfqX!qL9CchDDzb^Yo~e<`NBpb%ZdcniSXpS0eGlGu&hN(3 z)`(x@AK#tK?mP2Eca5JuW2-a0l|5-_!N$8bT=p-(do(5RLh)}1(w>QWHuDyq_YHnA z^DMA47S1_e*!sKIk{y{34xMzUYccDFSa)X7eXD-2bKYI(ZQ$aL?7lTGupa5o3<;Qu z?bK0uJs-~J-{<9DO@5CrpTC9X)nDu7pTYAUUw%nG|9&t3LGY!0`Ftj*LjFf-Y$W=y zBE6(eW9ORO4FOX!n(?Nw#ix}@&W!xRkq(^ud+su@7Wh6eIexF3&!?j*YVYa#-9LVA z>+e2yOV{sS{KAgk|Ld(geqZ#3Gk>psZaa0W^Fi0%cw_u@_%dzi_TLNNoy3{*G98rB z+@yJ;LTRzhmZp{IC3ZYt?9$}5$>a)6dmZtA2()Y>9Mcb5XPmFj1-6dbedl-m2Uk;P zEcfw(_dRz=^O@|EoWS6&q{kC_?jV>VkJP%Xj2(?oFa3b*X(BeU+C*! z-$`gew(!)W|A>#4UqVY0b$$je_2DSQ=C=PeV8LMAP>y?W<2v@A(tZ z{w(#1sV6^V1U&x``dNJ6nZGyGH`rVGYtnn^dhovce|I?fQ}?`TChe&Gw*-B9{{+vs z`f$F(vx^fwcp96dz2-d5;1}-yo$%&yUSx6ZEx`F8&$E3v5BP8{0Ox{t!FjsH`NIO7 z%Xl6GPQw{8J{Q01;`5oeyRHtI6qZM~4_3d)JolS`*@FG*1Wp6i0N>^`X5z0|X9M^D zd%&bwLGJ{v1vUb!fpOrQY5dOp)CKOx#^m`d8RdCW6FbeRiJo>+p8N-TAwI7wpno>c z|IPc$d7NMK;oJhwE$@Q!-!0C23UGdk=d=&!{XU$%;Ou=DoVzSe>9AiPDtZ2m4`;g% zXVv)AoT@P=@B330XH5alkv#va4`+i9XA+#rcft9H#d%2q&cBlWPd=QV_2JwN&fV{V zbB)FM@dBK0k=_MPjmuYw%cevooOYZ|`RiYgw<(hCYt+#`hWsquJI`@|vfU)LC=A=Xc3Qzv#{roDWzZ@dfNlHG9`pu@za#B*PqLV)wiDax`hr$k*3girqSJ z8t)oLpU)J-6L_);p%J>-u||rm8CVl(ob0ToPbubM`m5(3 zebV!pUd0dW1i$$7mS+du{C1oFCi(t0e%s&wOegf~4ChDWsUG)KDpKIkdzgB@nP>G~ zyxU`*r;nYyYx&jmflJ|~c_HKH6MOR&O5YQG!Q~b2sFrjwXO!DHLpt(#JDo^>ze5`6I}=P{yJ>v`p^IDdw7 zCi#KR>$24UWFcMBJ+*(0E4P{~`%w5Iyzg#C!sCu~8Rx9L)0O)YIONCr`LA`4_^)>E zhgUNrM|o93dF?~&BY(4nZv(}*=dU};H$6Yc^KYpyI;0P}r}Y^2UF-iiYdVcV`Gy*Y zm5jqL;2(zBR}97(i-U(cogW(Nd;=U>|GdLKqxhpA&Yv*|-%a4L5zaLmQ4vr9j`I=dbE!qK=pmV5QidF*TK`|}P4hW6oqO>iP(^%~m z1gGZsCx{=xhf=(Yc-t!W^(njHL!+F=Guc}+^E|s?@9tHCtBjd9l$@6iBxM_Fa${r{ zsN>p&hn8oH;ORgP8^dt;6fPLfqyW0DbJ2R%v^P@B;{{S9} zN1ErQ51ieHit&MNrH{QD)A+bY#{4uBjoj}%0WbPTnp_xvsJQNaNB&R|IR12y?>_)L z!R6k`iV{aYb9feQ!dnAALC%!lp#7Y9m4Q!%ywEsP|CYduF58|l%mp!Ty|T}?bw@#4 z)wZoXuX+FI<#9fnBzn%HOgHVOz^k)5`O4A*Wc9`t(!=qmT>Z%F4Cfvd(zB0JPc(fQ zo^Rv%SMxZR&->!MFQ&9#6r{$iVoqng%C2k#x9myPjYsXt7i3TF*8KjUQ`G6%k~@*V zYfJ98wuCu4Z%d?yd0X;BX!hHZ55oJlcd;dx(l6PPdBCL?ugPXUye7NlB({Wm0In@r zJN{T(vIRc=v9JGN!aR7)Skc}#w!&T8OYf!Q)wCn{x*#$}Uo+b0@8)@0r)<%6xj6H_ z=9!q)E599*Pf@^mogYs9x@$U`TE9^;$#sc3E;@v~ur??OqGEVRQ8Ta&i zW@gJ76MM|g%;CUKS^MwCKIsk=_u!x-@3Y_iY07eMO>?>E*F6BOWxCJw?h6#zzI?5q zFHJma91UU5nESZTh(DhvO?13b&37DlW<&CrF`%>$6r=^ZQ@J0YbIKb=-bEJ2OW@Gl z=)Qm{CAhw1-18+}eXCI@7#@d1p0J0(%ZuWue~3M zE)4A9u~x4L=i9)>j^pf5^ML#U-7^s#NsR;cOWLd-mbts*RK`ZhXzpmVjDL`o+Z=p5 z&f-R_aux(0be8fhksqLk!m}W-${EfYyqvv-#2D?d&(3JSxFfkdn;5^!x$e?c*$`u^ zcv&hl{mE7A2|w(Nr>@{Y#w+1nK~u-qI{#KLhllnf=}<{1Y>iudY3prpY(3d z!r`m3(c8_zs|Q+Ur$z-=%}T#_{2o zj(-t*Iy<+5b~Vo~f47q;CCds+1WQz-%|1R*<+U0`zgQEmwvWQXAh`kyDxpL zO@E&BExz=R+VlrVU+qgTx9KmDt~Ry*KHa7*Cavy#Z{F8k&xvNU6Ij!c74H(&405+XV8*wU`PjdKa<%NV~l&AFMFoBpEX4; z6ou#HA4y*3`a^$W@2rk-zMAr-$WM8}Gbsz5y`&8$Z47CHd}%?_mKLP_?^}|2+Fc`= z2s@kT9r~8ldy7ZRgWXvkooG&^Dki+PtXIIW3Zc?dS{`-m7f}8KgW%J*R)Ato8 zrucRoLEl^aCHeZ6d_h-&4QUNx*;ksUvvS9o-OZj*e|`&z9vghqxyvnc=sWY;*DO9Xn)gV+aW`p_Nu|zrk;hncdF^Rl zdx!qoU!l7Z%g6tl(;pj|l`X1f&d}I#_k3%(yNVoj*PsZR!*yHS_w2Rzr{@chd$>mT ze<%}6~{ul12j{x^elv!=@=??bA;1C^;Tw@NtC>@)6bU%Ij z-hc_eesn**nn^4|c;{!_>G(Nkl?ud<5axe|`y;QiSB0HV65_A155-1&JE5BRJjEZn z#NFfWo*d$P^4=NF?pN5);@O=OlHQy!n)~;o@k92;f0B_d#IU_O+o@uXXob#+v2Le2 zwVH7}Cs&f--D&h9g5C^5_Mt@TtBlw0ur~g>^4SaE8%)<{^lpOA8s(>7#U1~uPVes1 zujq4m^g;JsB>ABRxs;hz*@L~T+aldgO{d!7Y>Rey7qDc{nCF>WX0t!>S~{S%BJ00X z99d6Wv-Euq+PV&!RqjRR?76H>UQeHkzYs;ftHCMRsf9%N@QHF5~_ z2HY?BdEd0z6Zp|UJ@TJI_Z}4=hs5x8BJ6`hXB}Utoh)dchAoxQX%S%(B17iY>W0@Sl3j{ zwebLP_M7@bt_+UyvN*QkJK*?wpa-7zq#l(l{)|rOeQKRcAQyDUYm*^@4>?=!$hhh9 zF?*o>QQDkOJCc!GHu7qkJ4TN~Q%-&K({K(n=zB{8^6Ys%UEBi=Jyv z5pNCY-Usch$&QhJF{j74;}yk6)7`p3#@-i2$0SpoXB(sYA>OzDH2-vs=ldSE++5n! znf5X7>VPZQ^U#49x*UpaIdItg)b-6ZS7NW-^CGpggL1Km_wG^t-NZwm44Eymk0^NF zR_6O_;`BA?^+~#N_#);ZXx4s@=J%_xeHrHcW%eF1_Xc=}g>`6@eo;<#{7JrFp><1O zdI$HjS)1d(ltD-SJFp{}gHG1o-h%ede;@5}pZ~b_bl2=e?Ma@Qmy+3K&+m8ion7gs z=4g$lJb%=G$z1n#unQTZ`$K#qQ@rUK&pko>Q|;;HH%_= z&wfd<`r;GF+4nA;&X<>s|An)9miRCy zr?rmwkYB6FPcbRoDcf{lBKm6*dap(M=3bivJ}Gy-xQAFVD9X3p$d6NR8h6=LFA$CA zbhl7CAeo3C?%i;BQk~=uA@*ltWYp`IXwN1tACY&bQrJkwy6*01{;I-GiB8eTny%zc zo%Bohgblu?Ygc-_ zdnD3t*$vq#licFcCH>akxOnZiV|gBFuGF`9bg$R2zA3b0d3!gXHs`y%^u2TF^742) zrm^*~%gb(8rs7ed9h>qg{JIN09T&g+_AGO;bo(_|XY_4h`F1)tYJ+d`7fNJj;N9rN znrwA)P1d{ny4me}UatfCW>=hjLTsGm_{TZ&`yO(NYv2vKIFxkxe6EjGlX{hUKgQ>K zU3v2PirhV2`Fz-R^cZC?i8Dsz-)Q`9z0mXVAswHMvvcrs?XIsaHY2OKI2mU@9e-g7 zVfKOkgTvvo=vixqI-lWP*#OV%+u~!@K$ z6Mx%(up=(NXRh{*O2V;eX}v$P8r^i?A7L!w%dpoxC)Y)r&|9Is(<&G&;KW17T?Gj8n&~Es93g7SVsFoi@6R&X>Gz2`-^e<>|G~VcMLA3_PD} zjkAO@ozTD0oX?F?qZ>LUU z>~n5k)V}WgXx^6Y=J&Ppb7Z#n4RYzle99C7^X+M!o!?{6e=~Ft{oBQPg6wN3x#_?(`pVm1$Jklv z1v)-*S{-(l^7;As61`Vp`$Bq@X z^(pBTbf3n2+?!m*-?l&}koOf|S_N&`^ZfUS&fCoRvG0SOwvza)fo}=OR}H&o8P{g? zj*HgCS`*LsNdBFnZgA?GM5|pK`hGz;xtjgUYutCcTwj;IKFu7YZ?lVLQ?t?0J0Q|M z-J8=MX3*I6bDi!?SvhxZ@!q|loZfR%xp1fae{UV_zL!Cs*3RDrpZfa8+2&vcvX(r= zf5}60-9-45pYQZ;qWCjW^PPK^GbhLU;IG5mH?&_<4R7^+_}u7F-edYVXUJFA9}s^A zBZvGsFFM71;nA6&FXX51acwwz56p$K$9exm`t8nxv6^fRI)6H{QT@45Z!T26`sJ2^ zHWmmKT9c~};+gXE{z=}>w_v|pT>HHV|HPXMYkKILYs0r3Hy?^WXSwgbX+O}L5A$t` zC+hRR(udT9keT@7fIHvGj;9I7&3o2*+z!7Y^{Z5whdZyBJ|2p^E(!{f&gf z;hmWygs*_Btd8$ZBsZ}}?P6}bCNn6N8eg1BjnR92A$TU5G-v3}zjUx9%9#LZA4bol zNBZtFf5xta`&-W_&#eP5N7#IKO-s3=RO+i3^JgmY+hgpjtscVODE>J8ps$=2f@2E( zm7FdEYR<{qoZ{GL(y|5I%YJ5%nMT~h$NAvnktAm>@N)w7Wv4>aElVR_^7+(nw!dE zcCUi7yK&fp2r~Ez^|h}?9ACJ@p$+%0|7@k#kq#|4%T7i2xiTt`?sOV>);p6C)9E~9 z+uw=J;~qv|ar86vd3H|wgZxd8tb8`@b-X&1bCV={94X0pp3X-qzDOC-CSGG#OC-ax z8uh&!K6^5w-)^5vB?lwiu3z^!a?LTf_9?AL?E)VFe#Z9sDPTFYMbNPb_M_F(`Fq;c z*`j31Jw11Q&Aq$6$>jf(I(whD=YMhJQity0pUwX1jO^@l2>;GEnHlTloI{O{UdDJ` z>zMd5C!b$Q`Z%TYPN<2jb?%*|d3Cefzb}Nan`f=eCbWKnKX-+gm(tuJnYuHRAGd+W zXJp5wuE-t>ZB=>Hd>O^cE*_4MlNA%;Vvw!n|#gLnRGWq7;M-K!| zh3ZGGJPPqIm#>8HC7D)Qy7O|#>vSb`WBEMF<>j95i)24gR^_ZtgksCwJ2C_K;`q17 zME3ko=Q0+dD`xvD-W@)_gunfB8EI3priSm0!NYa*^R3*`HCilO!ug4}Mq8#ak;j2e zg@&S;ob9R&@$}H-65gqwlnvuQ=Iuvj)(@edA-u7kdD1vN_`W3-SP9H_lzr?FOdF3vFC$+ zJIfvKJl{JvI?&fAJ=wQVc!izHze@`HzA>`Mr%w#?4K3+%SIJapU5!<7NrAXrN!w zpF85q-s8s4u1x;^>Cy4i&G^~n>R_1jGHy{{y(gA^nV}vzmEEO{m=Jh5Bjgasj}VPo%xT9 z*B4htR{u4=`cxNK2`u_Depl!8Y&1HZJ>pd6L;ktJ@HZ$U(_KGO<2PDX;)q_^`?1g( zr$5g?lSyME@GB<%nEegt*592hCohT)4mZ#C8uI{l`2p@=>_=}O;10$EPJC>I^K^WF zh83z4rOpQpWp+7->;^$a9 zPDVFYIMI#@XLMpRdw@r6xp;VtF6!L(@37?q?a-d|2i-y+btZkRJs(DTB=VqZ&%a>X z=8mzekNoqDbgmr!4`CfBTmEJ4?aPjDM|Ryl+se6`t6Q$^L3Vx})i|4o{3gQlLFkwI z&3*J^?3%`R02xM*o9fE8$&YpA$viSqy2rcJo^Pjjste|F^iw($IEOtot25H?;@EU= zZi&}qi@Qqj3zj)wqkl)B-?c?Gthv#HYpgBO9U<|52;bECAvOqEx^s7Ut|#xJ{M`L6 z^46T21dm_dVdPzW0(pP*|0j9NrkqILr;whPcRxH(UpW`{`fTN$m-U1H>&f~8%60KK z9rJdCy|;IjbrrI%Le}^3caYR)cRa(#YQ`#S^z7^4t7c3U!*@eD)}!v)`d+2aC7ryK z^us@17$-N`G zz4_AhOCny}tslp~M&EaR&hroRZTur`2(R+S72y6Yxbyofg?-Tevx)BY?tqNF!cm_d zGoC;2&uPX+xWe(qTTSLA{P|yy_9A5l`o~x}9ID_OdK%X??p?ee7Ub_EUp~9?HAj7q z`~iL>Had?ZOU&b6Xu9)_RP>*x_if{5Pe{M6d=6jq%Jk2abh(F1Co_9wBb&}h9 zr~7*Lr(=_{`Zj>p(!scQR#nV;LEp7Rzxvp#a=%yk3E+4_HGzGhKHlT(n?f+e^)BK# zzBd(TeXqS=eM?yN*%R%%fig=7cM=v70?|p?Q`z&=+D!X>9e+lb6MVBH$elUlDw`ua zEcxt&hwl3o?z^5H1Mhk+qHO+M&$C(A=v_~hf9f~<{m4D||1+NJ{ZyL$e|-~=`*^2N zz7JgPdJx>qr+tdI>pf@kbtk9u!#YFs?o{i$LpPucy7Qy&*!_q$+Q|Q)%~xArISFl7 z`e@^#$^dO^p{<-cvdeiIuej3mMZtTD5AUjz;Jv_y_go*|JHZ>I&i?YF^ozbbNZUe7L89cQ2u;0QcqK8A}>| zS5EiX)tAdof}2n24CuiOAMWwseU6a+(6Ky;foBhCe%zlp32v^w58(c|4|fH4|ADZ! z0QZN%vxKxkKDx_Jf?N8O*WZ8-cL==e2-V;@)rUv&J<&MW?!$w_GoWjK{B{4qFM{Xo z4<4(the~IRpt9#kM6Eiw@HO`v`L~lc_>YYQb5p%87(AYBpIiG4j@J> z6g6N5O9e#-u#)c1%MdE02>}^WI;e;Yw$MO`C>6rXh#Cklk->U!(xJmR@r{V){{H)% zQ(fKZhB$NQemCEDzUrK_A8W6@*4k^Wz4qGIs4V+PPZPuKotPtMjq)eG8Aj;k@*G-~SrHR|B2ewOijlOD>`X{x|Uabp5vg z_Dg>U40UzTldHkEi(el4O@1I`>X7oavcK`hGS?r=k~s(C{^1dDu*1i}EWh5&5$f$7 zp_P_D%v#%=OklnM)r7K@Xmb8EeFP6&V+n-bZ66#N`lOa0=gDF%`pzK8nbcjUC z%Kg>6Pg9mX9J>~n4Cmui7FzZ4KMdwt(4JM0s~}}%n3rqe*8e~^^T+x%)djbE?cTHe z%!B;?)CgmJjCzlc7hVR}qAj$c`5gzWp)akfT!al4z`I241LKX&jI10Qc*1JE=I7M2a(CES*n4Pa#VFtI;M=2oo6qk_ zJn!X^Jc@E(=Ue%^cQUzGZY4*-zlThbXEo2}Z*oqJ-`4Mtvl_JWt>37%XddD?kip5GYOL9eJE`2-vE~3mW$!7Nv;1F%3LX+J;^SAgf30@qML`Z<;MU&Q!e3`!#5n4k2KD^CWqXy_yN>Tga7@bd|yOc7M4UO{+`Z? z&T-5g4=jvV zyv~;=YfG1=*;}Bz;;*J8pWtsVj_ioU6Zr1YM;OCfsH1(-7W!3NnhSmVwtOVAKiE@N zUtdw%&eiVT8V?ay=j#{j(_47zd2f%}wEVNmo+XcRmi$H9xYqIyFxqcxYv;^y_U#kj zX)cG?dya3L`S#EJil=Vi_beXG)1CZUJlmL|6{Y#$S@><87I7EQmhh}O6D@s*@t#6? z&Y=!yP4D4w$C9uy_wy`5kyyZU3xBWX_YwH3o!@8qzjB9}6!eL0$(~|4&i%EDt5QrA z`rf3JA6LL0hrO5O`afdFpf^aSXzx9^Lq2p0dFOa9JV&|9SKnI?9N5Pmxs_9YCO_JA z;0y6SxWA?T?iTybAtso$>1lIcS@*lhPw(B8l1BY5bcXOD5!J%JXCd-`tFDbJOP*TM zp1JnP6~E98P2takXGLSD6R*-dQF}wd|2aQ6@niabPT$X}w&$8B5??o-ec0MNt!*uN z#ZP?5=JjmJUd!KS+P$2XjN%8*P|SL~C8hPEqspG6rLz|H{>#jz&a6)%21Y)HSV8Bq zP5isO7hW~jCUtg&xt`R@danI$Th6v+;$P>x=x2+{WjZS4IH24$l;a$t+QUz&_O9W( zXzps2%XU-%I8CQB)VD5m@F^(bi;iV z6FFNFZ^?>I&aBSPHcxDVPB=eOdzbXzDF&!UPx{-ZovF60M?3N{2edOzf2Tz|i|zhn zOX6zFbI$^Atl*tln=07TXIsBXjIyGMNqU!AtT_8gv9uR|K2h(oi*25aSo&I5bDhw; z{Nf5e7Rq_C!_9h^TU>dHcZz+AA%nH1Ht2nFF?&JH+Si+;*qq?o-+t+R(>x)I?Rnea z+n@Yzy0_yFzUk~v@NK{UO?bbVZ#BFtpW6p=73FV}4yQYJZk$Q}0Y87R=Zs&0j^C7<;zjrzg}`YO%%BJj&1U%Zn#fR&`#X%n+WWuKIeA z_iE#QzYX!E@T@$8Bhd(lqYjcSJF$5K*&>>e-zcDk*}iOfQ1k7}n-%)orw=Q0eA#le z-utq}(v6h|9rmoH#-O$C7s%uwj!kmq3I4A=I%K)!PmO7VKc*|~n9jDe(hfe8==?F- zeAw36L>*7=s7@DUHSQIvgUk`#v~%_@sl5;6%@1syYpCPNAl3OIWrg31RR{UQ8aXM6 z%!%3YbX40jYbut`fn9Aw2SZ^0)(EhFjJ7l%8MTRw6rCr`we;)BH1+!-zKO=qRGIvl z70H|=@mn#A(=)2=+0~Vi@F!T;41=M21Q<^AVR%m+4E6ZC#MT*#zd4lE7>`vQAAgDg zYx8BS;693PlCMXqOzuM!V$m$GB{S%v1<4p-=ghfuqL!>=!wtPCYspq@8z;F!nz@=vVS5S_3Po`KLI35mWqtCYIJ1dIm5Aa}lBPTqZ<>N$a z)*wzq4~Ny`1l|}s0#0_)w{Y^XVDRay-LMayfPPZME!#Rn=k14-70!R4I;@MsdE@tg z6Auo>V=rZe$6HmW#vAaTomY4xx3b0?Y8(H_5Z)j^+ChB%HQEyF*Q(7LZw#fwtN125 z%&Sbs;|=9VNSL8>v6}B%yI1n7xsskFIgSs*t_}6`p?nbS%wrex5h<{rlWul{d9m_~ zbx7-&H%GDqMxG<-?%$~&Cv^MG5yp8gFln40)fj4YJ2*$|tAd-MbJRgu;qf%psm)P0 z^LjP&dbRsiTjy%%Q8~(Vj@889Pg%*-_o+@gkYC6+x($zGgym;uC;gaO?c~5nbet; zpV6Nxs&`MdO!z!p=v`S_t*%Iw8&i#d`Zr&a$^{pP4!)LOv(&HSyHbKUD3b2dNF z;%p`hLSY9sn8gF@vjBN5$zmfy7xYa{i^>9o+$7z{u)fNr214I zE;DJo`bw%VVkE#BzM(r>oQ>b4uiNSC-|G8{026w1NcMFjaH;(&d;YOd_g-|Iff(~4 zStvUDfbf^dyTpGC7 z%l-t;J}+kwcL|1%)ck?RXoeWm+*K}c4m3H|_i4|PivBfUimS2nmh9o2e$O{U3Gkc{ zo=>kTz9y2w|A4GYu#Vfa2wBb|WDC=z69K1<%m1}tv@a(xNzROP|UrTVd$`sQThw(3^{4FY<@pE34&?B8I zu$eaQ-OT&e+zs?`WvL0?XrYZpU}vAPJePB_CXue-CyLAO-@rS4TXH)5z*v+oMYvRa zjK1qzaAt14ADGDV)s9I%=@{#)3)TLGNuUYhG^@A{0_=F_Piz_A#f?TF~SI`ej2C`G$z@BXy}e$g%C zZYxJ~`BFpv>!r;<{|4ok)XvpK$;a71K2Eh0WsHi8?Ro0JKIJNH7X1|2gUA-DV@sv# z?1bIJE{Dvfew7oxV%aO)Xz>bH_fjtswzy zUfyp?UGAsxrS+zFGk5Svkuvya?ihFPo~${rrv-XW8t&LpthgyuXC@a`bjM@V%y2`w zYx^&|a0T`>IUqP+Xy{+L;rck+{~&pQpttfd%(dE8-_jA6!4oR4|BL2=b9e0t_=j>@ z=Mx$76D5ETI7BBc_!e(VFE10r%3W~8tL4bGjy!q?@}LHHp~t=BqxbGnohZ3uMsii>9HmD@3fK0}DN3Kc z&b{oM<;28E?p@(Nz}Q67eZ=DoAKx51KJJMfK0d~!^)8WC?j_xWvZ~a=7$YWElHb3H z_TEfg`BFlhz0}#dS(ZQWOVCz0qnx#@58wvh$t=l)-sEm_x9{&q)>V~H+wMtZ0+=_s zm)#2ffH};)rGk4WaK}f1yV1gpJ~I;B%!^H1Lk@-MBk$G;c#4P;b6@l2@c*_`UZ{&^cgbCku_b&{QEdBEE+zU++51N%R20I zmvpvG`k_g#EFp6VW8!b>9wodNDVOxl^V#*utSfzlz5^LKg#PJEd)NUlOhHEIe$r@S zh8woHUT1L~6|U=Kl<+Kk4#l&(|TR*k!T{nS9~d% zEjqu1cnhta^N^KSLaWl-d()3u`qtS;?PrD^Xl3fXdp?^uum^poJePM2#;z4}C9{|h z$>7_OzdNlwDrgUcIBlJBVSf^6TjeKZ#26CzG7_P{v7z% z=p4Dj-6p{O&MWWT!`a?vBn!1hY3^5ZuZHr1Ty_q5JTe>l8%5*Tq4hKn$nyI6&^mSB zlafCp$io8pW`WgYq0fv@?}aWm>a65k2=tm?+pqbGIjbyvYwyscSC`QJ*t1$}>D$T# z)*0=YMxAMHiT&Yu;6QLjGFSBrCiN{Itf!mdFkbsAFj|^PBNKp4{kQ0E+Ox5mW^HLL zb+y(g--^xzi4Qfe*k$Ns{+a`AC@->ja^3JXXBf`~IB#Vi-Qs+`hx0IN*>Idc%=iS$ z@bP@&)y9(s*Ew%IqNC5+wdW~Udy`*8PYxxYf|j3hBQsZ&Kj+-LM|4A(Nvn}{?8jFW z_i}@6KVAJa@1ur%=*H~(VO=|)2-?Z3oi(tU=82GzT?u3j^3M|#$jzXZ&$qmTK(p!Ri#?C1P)@A2W4 zuVn-6hW&oNNIzEyeq_%~f4{Po`==ZV`#V^+ z+w~Tj2-aK4j6V@)^LpTwe^&77?&gzycqi}}4J;Q`MiJiV59NpGD_q1Pg_q9w_4*75X47vp_r47gfKtjX!l z{;<}YqSlmjIaO>%Zgshrhs@dpd>T%#Lw{Oa?#(``v+y-Jv8>%+C(_bo)i(DE9-@wN z_Y<0be1eIgx@-foN`4u^VsVy~uDz}jPH;!AbO?Cyo@^&$>~hZlK8>l7G4*D5m91>> zW!SDG4u0rlLKM=6KQ)4hHgRO&oB0EKE z^DB=-^WekUBvz-E&#KG4QTjCPO25z^qV&)^j=p!#=N#!3rRP?R!X~|9 zMH!p!FjFW@z^0pc1#34hW{zK}Vv=d1K_8+K|Vc(pbh@1GbA|^*V)aW9>rOSgf0xugfmSE|mPTy5;CPyyooRg*)mDmlu}a!Hd=Lz@>6> zMWL~1X1N__I3c~q)8&PS+tNNv@Zc@Pj#|4<^6PTZJUQJ3+abcV+QM|O46`sL24M>D zBOC_!smIMs!6BQu9yhX?1&8H94>u#(6~fIYUkf*BPZlWWO$d87L>?32MfS~Yxz4hD zg;s9?k74LTH1p5Hbr$CEQgkf~XELypH+plJz|PYgVkb*q4(zazZ2Wft>pbLwok#p< z8e`3m!F%nm2XiTWeM)O|a?nPJGH=oo#<6~jXQWezFF1!&I_^F9?g=?7$O#rSxeuZ3XVEc!`imnRFMjZ5m~&U#PoC})^> z!4y1RmyR!8f4H9cX^XRiWlmVW9q~_EFnV+${s|lIuz{bo=H4N=7~e613%x|PUvKVF zPj7<1WlIJ8?dc;M$rZ&q09;lM`LK;9hvoyDazTyC+XmRck7!nMHjEA&CP$WV-isVM z3Va9p`5RvG{dve0oeKt+`1pJFdh&_ACB}2za2W+|r)Uh!X+$#J$E~%a5*}^?`)MTE z)H(`oLji8>{QJ0t29_h6woZS%thG;jBwAmz@6i~#x_>5e3>P$eFx&~8Jl&mzt<^4i zO724UI=!l#NulB{{It8=mN}j12&a|rwe~=+a*~B3UBwQVG@Ev`xAH;i_Re^`y!Fa^ z_gs?m`iarc@IFqc>*Kp4^g*l-Hb<^9%kKkxPl#~-M|)v`%@x4-wYvH`^DH=J>t0Rn zH??uN-v)NoBx2-i@@Mn^K>pnR*vN8kBs+>Y6V?U~pV?7h-NAm~`IKXf)usGC3)NXa zXI=ib{1#7!h*!sIU!YWHKSBG0dji;n=n^J8VkN{RE%CBH9yxmcN1S@2v~c zjilRBCv(H#J^=db?5UEOzKr($h?d7B1AKqxXnc0TC^$l|^!&R?pU;p%>R`gva+qbx#wKHsW^K`kf*jd?n%>Cs{q38O&i%Z@lnAy7!Z~6L?UH9v3 zgpvG$(oz1w!hEpY3jERXaoTksm<7k3z$18XOD->O4Lx2qp+fokM-S}VT7JCT%KMNx z2ft;{6Q_jCGxM>_LPbwkw|7n&zpGg zdkx^D7|{Oc3?6+W2k*cec-Rjgn8-uymn}=2@c=f{K!S%o`dS(JF7N}f&x!xd$w32; zxef4(&eDdQLTA`1Yz!CIl|)-_3YnWD$oAdPqI8-4{DyLmv7SEW##)!7mo2aC^Xc^k zekBieepfmL`=BorQ!692!`nUGOjagaxsOp0M;Wj66_$SETv~tSGz8mVx)?9V@GH$qDWNl9F zCzw-co){sg;fJw1?}_;OQO47ctiPzRySypl*@#2*qk^X&o#yLD)_+qN)Q=J;)UDxm zkCd2fY)`8nL^x@yG~!o6^~spFl4P?6RmSl%+?C{~!PJ zpe`gIK6~2-=|WqE>q1G{OuM~(9s9Sp$0K@a$evV6(5L7{>-lfP%u&(vSBQRsJ)>Q~ zy!F(_JsP2WC@VkL>1EkX+Jlgd|4`W6Ec;96dP1x*S%q~<9x3GHlR3t52WkWtUs}Bu4cR>GP?dOD-KOFWZa2n)c_n$Q?}2Z*kv{>8^||U0EW3`d;}n z`?AlWN382_%#dHy+~78#<3!1Q8gte`59^34T<7+E_L1`IGmm)oJoF;@F$*2Y`fNOz zVaFel%+MUsXSgVxc}W>zyfaq9j^eJiv5db#_BF8F?yMg={t5p0>(v zEc;b^M7I^uP0bE$TIrO^_2&7m&;8p0VyFCh%dTKdXW~=XRkpE18O^t6Gb#4twj4T< z;ZC)`9a!6oZlJi5ZP>HYnPk(}WESf{iybevhMm(Df0sTweYEjrnA5C3rkIQM0$7e3YC|*yW+C`U4wtDtV@~Ce-X?>2<)Gu`wY-VYR-+ zI=g5z-T6;HdllUo_@uWbnm^6Am~mYfRubh_Ms zl>OoNchLy_9jYJop3(XfuLb=vn&&Zf-}@JI=NJ zjUv}IM(96QWQ?pepA7nhmX}QvUeUjD>QnmHp~olAw|$PFD`<=fzfb5%I^iqiYY67D zG~S;}tMmPR{^}!apCjme!CY#b{#<@9=#z1FO#knXGyZps^Swcz@an^d`Qx;>_4F{^ zIa@EoN1M<8{Hf%`D7X#g6WmS-`b%JM&QO1frwZuj(ERd`jn-F@c{GExBm3buuME!P zO%o2D$C};t8hY{_w$IV$QG2eYk-nKO0J=j+>xmQND+F0jjjc-oPC zzSr1(59X60K3~xg>x2Agix_Wj@);XDI;VgS106=T+ApxtMzXW7edWNb+S&5Kuy1R1 zd!0+s+8^vc$j)Xhnn=9Xq~-XVtUi`y&BA8YK5TDpx8iWTJ=l!m1<&y8*c^5&Fy~%5 zu-4>=K}dKpm2qpMHI~ zY~QBS#&^kQqFCYy)YZ8m?UP2>C&ee|+1luz_Dl=3H? zv#$|l?%u}#P2#?WQ1Z9-+so_|v?jgy%Qp6(+ftowkbC2XpBxy-AN#JM=LR^3rG7(c z{E*ybHXB?gk*&)4v~)RVNK)i$B_ayEf5ca zeyx4A_AI%rbL7f_w{?G`=udm$4fLn>Z9IFb%ibj*f2Z;}s9Y3%^`49u@2s(_{M-F= zA<^ZWnP-eTcVe6;-9`AI@gGmpzT6mxGh)O^hs?^##Xk|sD3!z>1NKBscemM!08B z{h>f}o?YX}yTQ2?jY(tB*k`kDtGyb1+qw08SId4JHglPuRv!FGL+1%yy9N6t<|xk< zIrdId>>_v4rmvKIDTlI!^5^J#{|0hR<$0I;1@v&1&G}HW^Tz!_DV=resLo-WlIZ~q z8e5Sv$&}9i4BjD#{~q7((6c}671*;sJt_~*1dAzgMw<0cXMb|MZ!H98f4c11ADt1? z*`E_Mhqbdmo82hy^liyW%o%u?d9^g@Dn07H=GwpbEwn*oGx;Xc< z+4=#4`&N!4?n`H3{}CFse3T>qw)in69(vmH3H#5l%0u_3pk?}NLmz0%b-6#^NnQs> z=lI}7Z{H)LJ&RDWP`wZNw?D*vL(cu>JTx+;ct3c#8Xpn(6V5fqj(mfhyUAYVz8!qO zLo}uTQ)Uup!RJ|?Wscz4U|wu4t`sq&jCr2%a?|Q>;mRa`?5^hT#3Kg|?7~UelYCe0 zj8p65i#jDr(t@#oy%}?W506z z2yVglJK{=ZpIO*T-g&TwB>c!2#uk~^UC4_CKioTo@tpUzcbTpSy#2>4@+?6+^NC?t z%6u(7*OMJnoZYU@QwfLUo{dMHuYptUo=zi2`J|9JiqFycpZTo)bJNXkMr}BSayEHjk7Ai4sjiahv?mJH ztuGqPUH*g0p8jr)g5`L*+G(+>c>SpAcfc0vgu3@}|~8HJl?>a0dRs z0>4YUN)2;+`lUNZ5-a=lx5>f_%umDIuKtMQy}Nr0F?asErw=EeczaL3bh;WYXQ>E285^a;vcVxrZANmal6W3T_v zO8Nw6q4pehRL;^z*#5o3jrhNzBlK;B+u;Ad(f@7of5ZOwG5-JK{%^DY8}Yxt&j0^d z|971K8%wW!Ub={I`g+~t2``@bp{`0RdiQK}w)v);z1eQp;hE!fb8euUb-A1ShPpYs z(G3oiL!sLxMxi{D-`PA#o-i_G9>4Q>7VvcNyc>K(&1&}o=If<~2g+yjJ7y-aJMiHC z#s{o!VX(C|PtklAI_nx}NB`$Oq2!uU)am3b{wnlSWF&f}+L8SFlUY+1tq2y~wGeT% zw((u_3>@00*S)hQwH#j5871xmrjLM*m-_NI`2Uh&|Gy*=wRTb>k*aQH?ta6wfSeF1 zaC3FY>{EV-cK-g$!^i`E+xY)W{4Jb5&Ap`J`FZz+O_ZQ-oRjAIGttI*0zRw1hi^7x z6<_^^`a1L4md{mmhB%RjK5|-bK3UN{e52jLXZ?TB+I;^GzRv+`a0g#EdxOOJRyh*{ z&7M&m!l*Rtw}ZofrL69Bd$l|E)Ti#)!~Thj4s_N~I$}!r zM^;LIyOeLlZq@GCQ>^P;|2y#rz>|hI%(jL?!Rn3?&c7pT;t_CoA!CqiT*j|(8q8C0 zuO4>cw%}epa9z7o4_zVNK>4>(uQi7}FGwdDdZ*sSlu?X~_K914csQSA;pwos3Y3dv z=$(4#vjf4MdYmV;ePq-}fj)BLC9e+jd&-oA!y)V-W?fzC%htyjHckIAatv4_2IGxJ=PpILY7aeiswGyb@r<@ZGT z3+~nvFAG<)ImDw4%$;oGG2lpd^NHU6NS*X)fo_GKZr1{GPOko8Omb87^|QFyCmEr? zB_pn+j^wCh5amY90R}I|wJroXzvL^?S`z%9yhi`4`2}}9Bw00>ha^^zoHg-{KCU)s z41RvW`W!CG{iSki_%0oHmCBK?iTECJy$t3r3i1nnh3`$2yG-TCVT4{YBo|kZU+`kS z3-6y-xxANOus%PJ@+zs{PxD=N!}%&lj-DXDpz?sUB@fOo_)*HqX8VZBCx_-29Iei2 zlvR$Yv|mR#rh@!}qt$sIWyOOh`E|srL4Lu}>dd9AbiB9wb)?&V8T6L`%&@>?(x5Ao{%|1 z@(cdT|29&7!KeLi!aF%vhTvT~-~TjENlth`zUUmGemQUQ|5cun+Zn&&uK(UVC8zk~ zDuc6MAtxp4YI#ch*wh=*l~3f}_yy^W?(eUZeY?0?0MGJeL^B)Q`RMNQJ#8TuVmyW1 zM1N48p@vjpLrePV?pXTjt5xSE`iSDoJd(H}-RY__vy7P~J?W+gkztt}x(Bi=ud_;L zlyyfs=L2UI7}p~DjuMLyMbA+_7_EI@V9d&CgkEUNpWI-stK~J~-c8Dm4<}D4=`MHg z9`_F79F(h6ce$teTkA1?8u~&noe?sHLP1_{I19i8l5BJpb2y_N#de{=OE*D}R(prYpZv&PjCP0_*ggGC$@K0?InaL_j1N*gBmamICe z6-VE;cKx5f8^G6W?v2$P%mc3W>uTe^CF+fPrXBZMJ8s7x_dSd|L_ACs ze(9TjKj+_cevUiD@gXP=^?6BhoX|%v^SP8cJkKoTj&S@Pn!Dta?nlAxDg3QK@V{Go{}OD78tPP#IUd0Gmcne-Lt^w3SBP+f+v;s{_79HNCj z`Uq%4^Dq)^h%T-mC+Ta^MVHFd(M2valrC2BU39Q4Oa9)>WcLl>xoyXO_R=-zmNb zJlq0I9fAuQ?BM@f`I~s(_||66S2hp$dx=TyeHXdg6PxiXY$nzL`Xg30lzC9T3hbzr zrTZgh+wIv+<%#OYdO*8g%4%v{0zM)?l8Ia5n z{r!P{M7PbO&}|d%TeF++!=5F5{+K4;wh+z6%=_*AqBqgE^548ea{%2o`ehq~dl7jr z`jwvb3%|eD^G^NFVxHo>Z$~yVE=$jhr2#r_Xiw%Elpl^<)NTkkRDTTh`{e zF%9DByFT2ng{K&?Jm9Eyclt{)VFh`;-ey}ukr9vR*8Vzc z?H#PO$i~H0+3&3x_HumO1#~0aHDOEr2D!MKwMgq$nl+1D3uVoN)>W-*9gM}EH(|at zkJ}?=mSQJvWUl2u!(JMH1i2fde198p8v(ukP5q{?Lsoio*5K1CacFiPr0>7ghkXO@ zG!MTSVIEj(EUoHpJ;@l&gX*3GT*4b`S#{kzwz!3&zqf&mZp&Qn&WN!;h8%1qr%M>P z`;J>!J{H-loG8cm>tZ5D++4w1b5KkMB6Vv{qYv9?Yfen#W6i;*G2)sNsata{S6OHh zII8RM*92?M4%X9%zvkex7_#Qbw<6wbe`lDz2-X+Ix#(Rd*<4xTEAgXblE$O)X&gz$ zrS%hE&;avMTR(RTSKxQFvCM^LHJ0coV~P4>kw4~G=taMKnCp4SaK(HrOHOwMQ@iP` zy!$WGKjmj4vHolvZ}@~OtZL0kXAxGaq#?;B|2BmTTz2rn<=o1Jt2+Zq11 zo#YXH7#JdyUzb=|na}%m;JJe}aS{BDJSc0A^v|@vF2j3gvilxzJrc|hjR5lxX#XzS zZ#mCfD?abfj8n!ujC%}e?a zWvu>Wu5cTYw!YP$o}zu>wWwd<2=u61=u`BxocHy#Bs#mvOm;UXkcCd(y(fK@+rwN3 zIT7Cgd^Z&*yLsN3_Zz2Q8z#=y6%jyOZW#+8@VyAh|IWy_fiw z>OAS7{Gxk=)^k2fI?qzRH~2b_U5E10c@|e6XAN2k9Lt!yF$GW03D%$=2jNDjSetZKtbT2}3wZJwY*pr9Q0G0Prt>a=Hbtv%rk~(0-Fb}pW~(2h ztGoPuXD}y{8`9mZKF|E5>*l8oTJm(##WkH&w2`5Hnz`!8fusD;xk~$EtIbt@sJ?Ka zKUc4&FYLe5+cQp{LN1^MA#=(5LS`!h@GPDLe|MZ5GR2cZ#z}Gi(|g$0#=wrF2kfwI zOIJOk>gX&aztYKG3K?v)PeNzu>b7C);CAZ#ch|wR4S8xwM)&6~Al0*xHSWE*E-5Mo+HoA8Q{h@Bd%=m}mEU zd;8jB?%|u>)$5ubUwo)s5`5hGFv_*!&>AAUYKsqdK*Noq;W~f7q1aWzlkS@*))C$8+Jo#W#TE%qvI{!> z{@Ay%c*@n`DUbz$P1S&m(HwWPW~*)=IMV%V(o4R{uXLyX^#9*K$~ya>#2i>1s_@#n z!CdEnfqI^oBEHqg!KivH_}@E_#yV8v7AO9E9fIlABNm)l1Iok zS8Z$VC8K2H^kj+WfCeP19w81i#5>mW@vN!iWq*lA6ay!ju66EN+LV6S!(2$mwRWRZ z?3WF>jCVSZ@*kA%O%VUcT>X^4m2YSc{}JWUq8ueV4tcGJ@}7%1AcPq>R>5;Zx@}+MF4#bWiEZfxdJBWwqvh z6nKPZ&876nK;QmYFpu;hoCu$7e<7xjv1#tkJtZuyDPVeudLpG^ey5=$G)ho$GV;KJDq3ix`XaQLXs}zx*XS8E6|92qt-M!$L3Exn zI+eR!ZS#NiJBR;k-k}Nf`Nicp>vR}6Z-kEiV>>oIut-MB9u84&I3C#tK4jbB-{C3M zUj(n}4BGS16?gk_zZ*EE&$Ahe_(FPt#(1=NkN8RWsVu*)==B)h**36WWVhc3yiwlY z2CUGog;Vf8NxRCmG>7&q&m=rti&w?J(t%~y-2!f3rmSq2EsU>^I?}t3q&>~)k(3c0 zRZh5E#s3Az4g6|7xRPJt?#C)e8S&gT=$$+R6`rDO!=ds^*(YNOE zA%5%2tYBOp1BM*s6jP=A1ABP7Xh(OhUrrl51MFSY`elE4l58*HYi*ma<5&CCL43v% z_A){#Xg5P_2JtF3W-iNJNtsp1&mQ;0!->(r2a^W>OTpK!rtSDNi8J9YNR#Vvo3FK`bpxR}Bc zVkXJ4p!__cB5m_7{|L5JVtL8FQ-8c`DPCC$IbEe#?moB4A)YQl?(6*aa>U%TSMe`l zv#LLA$R^^fD#gux{9itU6>e|hSsQb@HE|z&O0M1)iS0^sab}YE!O4mVTv1V-d-&dD z?%1mw0WGxEQe0Kixk;U$j1)B%>H>Gjbd?gtqT9szz;QktKQ!CRrOEhkfkS?3W4J4# zgMMhI&pZnMKWgFXM>HaDcI zu~K)XA-$>;&3C)X*FF{;jV7EquABaug=^W(Tij)nwzy+~BVJ%W?7UppA1V*?H}N!F>{~+=0P;g8AZz@^M3Bd`&qsyuIj%C zp4M2WeTOzVuh968wfJf9k@H@82|{o0th~~&P9wXV)R>0JACzDe)X`Vva4EG-}3SxO^ke#knj zeHlxag&wy^tm3_bL9|fnabFhilY7U`dyamg(N^aP<`7;@uiG;&E<5`Xm-|4rok!am zbCad>jqV*q+Rh2SK{)8E`s{%NJEb4!ZVuJ&qps@do7xEKf0O$5J#E}V+3>N0bF?%8 z?wQL)Y3=KA*41I3Hn+SY{8(9*gwC*E6?bV0T`s|map6XDruph%{#x-fYrLI|*W?vn z!uh29nu^A3^2kI}DDU9=zvw%-nWFE^;h*)LHiPe<_v<@#^@DF}=hMPxlJPO8ebb*P zHy75~Hf{sf7I<{4v%Nenes;B`(79)9W(RPan;YmmCj2o5yC=^aJq8Xme_D$I94%3q z47nsI|H{2yd9E?G@j9kZs6Id)?e_{_ab$FWn>qOn7JtIsJaDJ|)f(gJJ`2*#?zuKraB)?>W%+cL zu-C=jw6(!fe3##6DSc$}=#cmp&dW0<$}Ob)`~vsV=IZURtKEOFc(3#O4D%&eJIwAq zc>7zd?b8CkkK(wr_aL5^9d;~zc=oDS7J9a2l?yV?oQWr=`Tfnb@TaS99%hFDdu?w^ z^eEaAexDfY;c~t+*PgQv%=a3`(tvFzI~qOX=1V9eU416Hx^OD}JAwc63x7DUPxQGU0e#S3 ziS`!oeLn5THV_}nHo98x)4T`fO_UcuN;eWuD$Z4Oou&M@1alg`rmd6fVEdg9TSEpM z0N=T^A$^!R7frg$`Tr8?=pGuieO|)jceObUS>Nc(c=<^sKc?ikw+CF!#K#tb*J?Zf zegpa12rV%uRu`NBERs>`Te#vJN<5(Z&uTv4okX7VqlJ|fI}gYS>75xn9(p>o z=18`)*XV#3vx>7bUG4`}hIt}CN=b52<45M=Ki@Ku@515{l2tUA7j2=o^AGNU7Nz+cOPZ;HL?Dy@8cyuwdRXn;1S`- zu7g^0co+Qt-HaO?RG;T8koLyq%a#m~e2~4Pw&zk;a#nvEXA|?IIE~ZoojRIV-94pT z)6nxlaT}6z!mnspF)*?v$baFLpj3#X;rOxDfmP3N%x)T4k^jRozlS>^HRnzg*!O@ zoqCeL%fO?4Kf!O9GX|n}!FE=L{WKqc={)&;eVk^HyZ8);&>T3O5YSx%baw=FC!cCJ zeaI#-nS$2x_^!}{b_ zLhCkX)&>+OOfJKLdfK~JWs;&7ayjrVprhH0d#dQj@n|WaqZ$muXefv!5ig5RUr$?V zS8LVjvR9LZ@~Ql4-XtHy!=iP?DTu~oqmQA^r-5fabD?<>%^2|0!nk3wSbL3)?%6X3pZ>k)c`~{2Y7U1Ro%CaN;|$ zudvgEsnrn@Q&`2S1hB;;FHr6C7Bpo2v6T1T7RXqKEpP8)U-@408? zeBg^_Vs2=)Xzk4MpP)0X**aGsJ9X@hh3YQ)Xv%b3UOZ8@nomm+hj%&V*SFn1DB3_4 zy=1z}F=$ruC1hwTQK-J3Z-3$sC{U!i(F?;Osm+_Qaq zxevSU&fIqLk8dw;D{n8~cFgwjk9d@;#0dUkkDDW3W7t_&d5c+xEl7S8c#*sJ$nE*) z^U9rgTe3p=)5_Stle9P5o>_-3zNYdtIMx^=;8-$etUpGzsdXquIhA?ZS%6GQmm-Pn zR^~>9r<`;Z@Z{qz94c8n{k7#_90uS1sBh3}v*w3BZ2jW8%C4p#mzzq9O7H$Ja)KAh zH&tR>`5t#nlHcT@{Iv6x;s5^JuxZ9#Poeq-wMRMIUNE;sukO;Z#7zb7e0<5q3}YjN zoD}%#F1;RH>kO`!XEI&BEm^j>YCNt`Jr?+^9V}R~=(D!HRTZxd?w_Cy@wD1`LtQ(< zchHXLHjF-pah-n&Y^D z*~wOZtM4%ym!q}k8n*$yxbcU`8s=4gEh|rXeqE09Z58@*@QZ!Dzw*Q3YGTr@Y2c3ui53zN5uXisk1NVXbTlC&3hN$+= zc<-Dn_LlWMbX%L=+R4yC3*TFc-Logufnoj&Ef6@ zJCFD*lWQw?vbMD*4oALsm$o_GW$BETR#k>J61l_OcwuEpcA0#3(Ym_Qxl~7WgL;y= zzr9E681^hSA9>nJjnMR+S=!F-a2@AEHQsD;c3}G*?jDEyX&lDKZO-Ao?%|D3ch%I! zm-WUs2L92w1lz;lwFx~mPCLEiUlbikcRF`VXJy*EiH!n3CJT;oD=a<&T-v_a^ND@Wp$zLb#;Xc)v3pYbfEA3zrY2sSzP#XR=5y-#eCYh z@k^e33m3o-G}n~H$0KpvC(Ihm>d?$dNT-nGD)_JFM4|mF-({Qe{aCSvB78Z>;&;hjBhK#MzllX;q!T;~} z@z?wa>n`i;^h=Rz1+UB!<#!C$3gNe;d?v&^axY`V+_raY3Hem~S-H3PSUF5i0>KEZ z{gEVVev!K(fhi8%x9#b}o@ ziu2YEe21)*-get(?<+@&9!{*REJ+sq2wB8^Im2X;f?qQxR zH|Os=%Fp{^m-YT@%+o2VsxWLbRfZ+1s6qdaU(nytcNAuQC4_t%nH_(l|JX8 zvgWrDoT%+7UsN1Wu3~A@NVXT5+q?Ff>{D(~t})YHie(>!Hdj)X`AfjtS;ZH2m+np$ z`Ryvb4n1p}a%d#FuoH`JNOMiOB7Ti%po97Ev@Rd~jx)|dIl)cca1NZ5fE(X>pM$TI zw&~M;Teb(do#l2d(7N7Nly4p1tt0NyFTotQ0mCs< zky{DxTkt-d>$_akz%7-4xTRsH#9}n`O&Ntid+Cv(p4Bwx< zO81JvqghW@$R>;XFg6D;CRszpUx92FJiPn%)H?q1-@WD5x_6p4$~EP@wNm~OkMAbA zcW(>$ZjG;7Oww9Vx*K{E&4}Kk`Lb|AE zC^Bz#1%HF5V*$^%w|IJ|hl|7AFVw~}x(qr`WoT=-&iSdjx+BRS>70uLT>LNVoH_$h zFM}fJoL#vsuJq0T7e7buw(FGiP0h{q9xkRKkLSHPWM>@Ve+$Fuj1NFbi2_(*;~C1mD)C2U$RCu=(ai1T<#w3k1exVe7`n;XAO z-7ker%O$k+#jr^o7BVrib@Yu7nddK}o9Sw-!7mIME?w1Km}(7HCXOK5rIP zM)YXc#8uGwpiX>WUHy8R4C-oK==E_C%AA28YgI|M{arqP+O+^3R5rZ$vwlr%f|kVB zfh-zs3kB~Z;!ofkPLI(Q9v&<`!oMfHdv)b?qDAK97vHPr%Smp&HjXtu9tLGpFqX?z zhISG;$tduE9FQ%zysmD&j0)J#-qcWb1 z3KK_HFQY=}w8}^GhFkELu_li3W%rco)EZvVpRLX+9lVA0Mf!3hx@%v7HKsVIe+PEF zZ1p||8$I!ymm7K-xuFxv4ZY6#$}ie6)jb^fuh@%7vb%KG@*URaAbgE6-Thz{2kWLIaA#!3B_!5+4ztt*%fxbGNiHdT{dz}ugG}slG)D6 z^Q?o`Z;)J38H2pHc8u!e&^e!nmV8?iNj6|g*SPY_5v$<&O0naj1#C$8MRweuwn9G{ z_GoHT3e1aP4=U!GK@D?7__HjbDqv78ne0@bX9KyK1rFt25d=i7c4el zsAdE1AjbO|&W)}bv;i4|@{wo^x|>P;|CDu2dn4Mnlt1(FCj5|D&(CZ9oYc3zQ26zy ze3K8(`j7M2z@#`(H|29VquL2Rq9t;l zXS?CwZg)TVJOp>^F3#eT^Xzq7**BpNV#5a7!Eq1^W^-ZzcZj>?bsudAn8F!sfSmTn ze(4^@@C4U-XMHI=4gYFpRf+y9;Y4RiXN|+z&i+(r`3vg1L3=8c6&>pQJ9hN(+MiEk zy1RpY9N{VltiqA@>tYVMhVZG*P#k%PHo;G~@Ka!1lv7@rNdbO}SG#MmW8z<52Yz&> zJ7^mAMRxcg`M-S4U#uzPK z?cT5bCGey()6enVFt5=P_O#8D!aYZ;fB3X1YOb{3sQZeRkn;o|HDlM_G%-V!5c95c zUq2EWQJ#3+@1&fgqW%3}1#aN8w9Xza?e7qM8`_Dc3)^FiA(r&+s%2d- z3D#Tglswzywvanhx#ihU+u3S1x!>YFJZQ%P3&lSH9HLj~*5;Q9@>OUI5#ERBBb=t6 z>@VH0WCVAOnVjYnI5_M69zVzERQ*43-xKvYMnmMmHOx~i-*d-!=y_doT`6)5`|6yh z)_g^rHPG^z)ktPdzY|*VLbRw@8PPIpis+YL?pT197ni}&tI%@KLA3ms#$RCV%Gc4d zrJo+3mIrx#U1^==^|9qOKCiED1HQwQ`_|)iwLjSvuZ#8tXDr_ZpLAJT4uhu&qUAh# zRFU;ov`k;3<)e7d-HgPYDK_TRs@6*K>V<5M#Z;l3cDPf4xl~TY81`oiw73Y^g@cg! zJapO9Kx};jw9#PmQ-Ei30k!E4dEFn;$$UqSSy755C$L6!**rn>(bMw~t>`>dTvC~C z%qgOAaZ`VkTot+#fjcP2(?-0N@noSZ-2?3XUS)sv zUbzKD&r#s9w2Ul$9ds-?DBg*s^5`bOq!=ZS@8Ysor;Bg+f0XuhKZxl2_dXwrhUE{` zZ@`D*zYz1IcK3)TGV%qS;qkrb*!Bf3>-tjL(hbz#i+3E{AMt>e@5Pt4Kk+AW#W=_i zr3{rD#+0gx8Fm*wD&UoOP-H0T`$y^S2>L# zXs=s1mE1R4Yr5PQN2#YZ-L_3#wGjaq5$>;>hWuwR0~2^SS2b_|d})p^wCzZCkRyaR zt%yk>U!>dixIKIe@}g-RD&J^&UbqNY>fei38~FBm${XTBDK|mogZC?jwWIk8@S-*a zZ#d<_dkSTvz(SdgeCv$Z+?NH8Jf*?Ca*cU7b&w6w)clHMgVvy^vm1H=ZqwZ##jXp= z-Os8i9yX~{E0%|ebAZj`%K5CE9{O8&PoR5bHAdd+%<7*-ukb(g!LisAq zLnd8;uj9#7H*;MmPn}_GY)l5&wJuDe%nRVx@>53g?@?Dg^%xLrHk{p-=@8em>b1Kj0c+TP>cizBf zcsM&Y@KK&+JfG&dfaiRkr97YF`6SOLcs|1Oah{LyoXeBpQN43`rtk=dM`=!>`&jaU z{*ZZtm19Zb)3lZ~fV*Sx^&ZXNjcM}qB%gdb3Ku>)c?ip0ueaQ#P>i1H8 zGh6!g?os}qtt%U?gM)ahfwStq_0_dQ?mo_@U^nI#loSUcnX2*rSaj#l!#2uH851%N zPhSi!;pye+;#tI#;+f5(wL-Kdxz`5W_m;^a4K3fYU3(leT-o0a`s@^*mf{V>Q%UAr z?}ngJ$!O)CekGIVJ7=7M!R87lpAmOl#+OYaPqpD*H|NUI)Cs0)GMugI6O29NEA`Gs zs!nhgvTcNS`mXblQ~0KG;vH)Pz=vu_Yl(6x$vzrvi+oC!H+!(RIIERWE{sCip-ceZ zQ+#(a%8vrfqLT#tMl4ABI4@s#+X3+~alOK+Lrey95n`WBG^4vCjagZ`ExEERxNc%h zsuwjAs>sbrF@A|bW*vp^{rXF(ZxXEMfh=OK!)7IG`*v4z)s`jBk@6ELe*hT(-&{Ah zZvI6Ze=)1v1IdT%nAP^Je!q(Qd~um~j(2lY$UNK#q&zX6z%EkkRy4K2?R`-CQb$#O z5S_=+z@gd4W&)#=PdS*6 zaI&*{``?%gbd2wUW4#Z9=il=8i`=KCeUuACTl8Ju*TZk#;y!%*!F>h&JUc=^*ZTcz zsO#sK{Jq-m=Xr3@@%g-+uOp_JeP_)eMjjn4YF1eI*9))EWx|oY)KQKk3Xc-2`T#k) zo>0G%K_S+!F3KgW{?bui4jdZWTHduXPojm1v|T?I*-}yVW7S4uu3&S!)cVMgmo2%m zn?6W?;`O2YLSfs-W@JuB8JQke43T7ofxe^oUgqTSM{v(Tia3;c`DXVa%6twy+Vj59 zf?}C-R6Wt5voxxbo-H)mersRA*shMq&ppiJz4G;RG?o79>F7k!oU_@+Kgy

R%*Q_HPxtmV(=8JV< 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 literal 0 HcmV?d00001 diff --git a/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so b/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so new file mode 100755 index 0000000000000000000000000000000000000000..4da269e734243a66e5eeff8f45e1bbaa27dcf984 GIT binary patch literal 262937 zcmdqKeSB2K^#{C3HX%UdMnH_OtQZx1!305x2xgNk+=X3;1QbzBLb8Eq-b{8OqQK%N z$aTA#`l~J4YE!G0TD7UIHh{t=APHz|z&Gj}5ns4#P$ODEeBt??nYo+EC9Af5{&+sm zwq)=5&N(w@&YU@OX70V~YKLojd|aHx_#{}SS%^wq$dPPO&^<3fTv;sHmdTbR{5{)p z2IEbPmYjHTd?J@KzAY9TW1=>a=qg*(n|&_BE#lidf|LHYv-yRPiw8w=3mjZ-frHB# z-!@{v$M{a>q{WJF`qxAa5AdDNVTSc^Y?FtFgAYsEj~ zvk4cQ@o$R&+VQ;&|9*e*>0kA{`uVVo$cJ~Q&)ZV6G4#c-8=oF^=;19-T=K757o55I zgZu7y{P3~;O%HyVT>eba^8M%DdC#AoD_UN^;?5hZwaSe+R|G5G&L^-O_d4rI5+lLp3r4t)mV{|wV^ zAlwy0pNC?MJ576=xwD{eh=FHAV4!xbG5R$mhMnie=hVLLf!a-hkq5H#)iLTX zj-mgs7KOHGk5SJzG58}X1J}Pk z2LCAFHvBVxX2t05;23s}kAd%sF&`(OL7TZz&^N~Lhr43Tm-Dfn4m4g%V))N1F>n{! z9y5SG<>=Qy{+S-5p71iuM1^UXIxF3$Oe*Hd%p7+JD!@L;!xMSeY#u%>;V(9+@=I%iA z^pzOn^%um|f&5`-4E-0x;9nR+|0`qI=ad-yXU4!YV$7Fptg{2ft9N4f!=xDV_sJOj zogPDPp91>~gg+A_4phfz_r4hT&=~#Q7=!=Y z82VorW8d+o81<};p?_z;&{!R>k zR$|oiW(+(fM%*|rhW?MmsOQBP^SU_(|2HxEH8w`SvSP#&cZ~Ya#(r>&WvHcj8U_d- zALQu+81W&+^71b^&OrRB7yPztj(bJ%Bk)hEr#FiOkHhcjvy=JTiTn2vumkb8lym+U zM7ud`okcLdn*%rVYAg@I{;8HPj(ZEv6$t$9A{1b#7Y{CvZMLoB{9@K93zc}zV{DwYs z_(Zrx;8QqkSt|6e5q3BoEcE$h3_G;5bz!Kb=q|1xoiWj8PK^3@iGF3T;{7ttf`$qE z*xu#xh8Wd~eZ$cCDX;T-YUjCgE2^tJ^NJT&czD^sC50vR#Z*CYMfnOa&RkYFSJaSOQCwH& zsk7AGR##G8Rc5KIsjc>UO1zfBg|iF1lgq2?3KtjGc_uom%DuUnj%5Skc?@T56nj1O z}s+S!>Ig}~~I7nd)~oPg`P^1>2otbro7Zr=dG>qR9VVuJswMCaRpcjDyg}RzP#19Fls5}ytTC@{M%by!3#^F zLRE1k{+1T!FC(nfS7W)guDaIC#42m(tEvRwC+M1a1@ty6N?j=~tnt-)3YV4FdVR$e z5QK%YV4d=kLfFY!QsB+ZELmDyTj;GVF82a(I3|@;c#3NsD49^`ge~#U3mZWe(B#6o zi^1z5W0cf-Dm_)+{8|0*s_Ie?v%jTI*oFySURa2FYCXl3kgW66dIxNKE~>hWt0%af z^9tu>6gmnnFGM?+74k0BO@uSlV1Nn=J(V@y+X`zvHSoIX+TtZ12kRu~z;Kf5q221+ z9CK$C6r+DsJgcB?B3!G~kw2@Xk}OWh6(!Y`HN~|aM`a~oUJKj@Vs)ISpzd`VU4G@u9~I>`o1iqpRShL;wo~b*`#hwjYss7BUivs6~!f@ zkCm3%YM;T3uA9%w-wuo8X3PIt%Iwyb=pK0#Ctclz21iyv~9Na3}A?3Qt`fI2~{bLL8G}d`~R~ z%#lw{XjE26{>(U-1-+M57S}+m+*E_yokXTo1v!Nn>j^Y$1>Ol+(e4U4_z))5;^GP#ip6A+f$GR8oLf+6sq|FxpoG{G z<@zsGzZ|0=RPa@m-|F+wToYPKDwKMR;4`ph|L@q5=3_r)$Z;n0xdv9BI&Wb)7PVr; z9_A!JqHT=9VCqJr3k4;P@Htr-os=xPlzJ+QtCnE#B)el}saakN6)Ytc)d)5Sb@)GD zDEO*_*=O<9dDyDuVJnlktnw@uS9Kn5IW1_lmYK!NiVLwi7h*{*E3fd>7TW7>t18Ji zC@gaY#g#RfJ3q<_aa!%!jL!H`Zk96q6t?P`ALsJadrEv>508s|2|+H8AM>vL(IpVp zpEnPlHBl@@WqjotaLMAX_9JWRL?l997FZ_nDtU#AePyUKvyc`gQtLlx&{!sElz25K zuH=2m&rji=SdGPk)&S^n8C&`10H{Rxqb`!?PAH_%fz*K-#Zr1jd0inB5-HYCX$9E@ z{FDW9)?=~S|4=u$g->Xmua4Yq16cJ$bMSgD=T0xADP$Q5qgY?q$cyo$B&57*N$lZg zDMfVn{$nXLFZ2;(#ca~o8?+r`aH9=looCrl7Lp+ZMPzD7VEQ74r?$4Lx{&(pEw8Sk zzyPGW7Pi2EVlijSG0K-byLT$Ti+3)hK5dlz;Keu$*$Z*co2=f12R+WiS=dHY!j{>?SJygC?+M z({RBHxQW@0)kwaa|3$C<(`2t-j`rWQ_dg^>{o4M|tqXrKlZt*rz@CLI1kwg?e174@rg~D5r}_KjlMwKH>L7tEknA4{HxH3 zV1TUiHYcS&Fc9C4(KYsC`1+Ck7&us5@?M7N_(3k(eU&2DrGQvkLDAJ> zUjpq4=Ty0>5t0qU?)4U9BRg}p({>N)0gx5uKNkX2t z&pD{Z{C=L@52Eep0VUbGo8$PLb45{7Ye+M$Oz}hSqVG|Mhx} z?!|X{IqqDf}ce__KvUY^{qeC7$| zR;fJR@-tDM0r?T-<+yW_XlWN1uP;I3w^}@+4D6QTqrbwz^I*%%0@GHH13k#{wxD1A z^DQ_TPqIV=?HzX?PWt05Ux8*H@q8>m_VLpLZ1^Ob@ca^vrnedj; z{6KxI3GZ#-c!mkj5a;caO?cO@IDfVY?|qo#stL~zb3EUK`)BcU`vqoP+?Odb;k|cq z{-q|oS=_g(G2z+5o_-T<(Cbb3uh#SWn@zaQ&hJBYn(!t;cbRaT7_Yr1JVW5!CcI`5 zA1~d6=ikWjUK8FT`gPQVTSWa9p|9arsRB1UC)I=-_8Du!Q(oov$uQxDeN;0p z=zJ5ND&oTe6K)fDkqNI^%k8k#gcmh(yvBsv1YU2#Ey8d8COqmtCOqmtCfu-3vk9*g z_Ss;JKJ7U+B|h!VQ1eV8Xiv-D1Lve$Vx3HQ_Y^*G>3IZ*V&&=k*`2Y(b}*aNDE2{xlO_ zBXFAu?-lKiHQ`MH&oJRdkMa6d6K)fF=9_TC4ogk=WB=m)sxje)|Ex3NMS^ZJ;n~k~ zeb$@sq758xHsMWw;`jy=-rUOZ789QF1jk!Vc(%w-!X`Xl=+kM!jd;>!!kdIWbrU{K zyl>QN!VUW*i}#+4INvS!QcZY~$aB(6xL@El6K>3}u_nB^Nz8W>UL)|yCft}`*(ThW zU#bZ=?2~W8jrmnSqrd+z5YO}YO?bY*n@qUnv{JS%ZZP3R;(4JK6TVmQx0>)Sfrm|a zhG@6bgoj1G)osEzoX+*pO?bA5*S#j(Vm#NR_P0Z`&?ni1cg^K|sV3Ye^qg$ME%P{k zwh7mTo~j8?75e0x@NU6hWWqB9zu$y+3;oxb@D@=|lL-$Ce7yOn9s4m(7HSMLs&#gj+;^v(32BQ#Ik)hF_WRUZMX| z6W%QJuQA~*0<0)Z?6e|RN$65{rR=Gcsx%v;eQnTHWU75foGWTzX?3sgl`mh zz6lQt+;751iafK$gzpsm-6s5Xfv4v8@7Kj|@_uET@Q($5jS2r!;LRr7D(dMn;U^2+ za!vnsPZxNG2_Gx)A`^a@z?)3CQ{Z6}e!akZP58|Mw+X*C@`2lo`o;P~QK1C?=-i7w z&0@Zqahq6wPQXQcGUM4IKAG{N7`TzAnE8#ohVZNK&(PDzAI*5ar2l;CkoYW!fiI1L z*TlfXQh(+8J7eHoG4K}2p5lm&Y1%2_e=?NEcejK;C*emWTz*$ORcg0g;$J872Rwd@ z)6>4sdI`s)u+dMmgv;+cbV+zxl-pw2E8(X}_)!VBNqCxAr%0c15^j_5izR%lgkLJ* z84^zCuf}JAgika;d@q&oNfKTo;g?Hzy@VUjT2pbCgwK@tGemr*epN{LWC^d7@C6dy zBH=|6UN7P6B-}6I%@W=q;Tt5pQNqJwU7&f?B;mb+ACF7pl)3M7RKgSbV2foYk9)+A zN42A$WC_OscJz}f;bbG@lP2L*m+`SlxDm^!Y^;RihCuX_A>o^&sKqi_!tts|^ph>& z;*oh)rb;*t^rN4A3CE-R(a!=2H)0hP7D+fBO^<$-O1N>9PlYuSZakAuc)f(5LV)=A zC7j+#Ha_bl{8R(PcawzwLBiKd_~{bfEa7KJ_y!3-Q^H#${45D?mGHAAJS^d(CA?F@ z&ynyh2_GZjdnKHEBroii@N|h^m+yjQ}{m++$!eu0ErX7&01SP4&-@Czk8Rl@%& z;b{_nk%ZeM{1OQtE8*iMJVU}WC491kKOy1S5LdEqGF2`ZNgin>~StsGsB)mz&ua@xj5}qyL%@S^x@C_3FX9;hS z@Ei$mmGE2%4@>xC65c7{c@o|w;SLGkE8){6yj#L&NVqQHs)YASxKqN9O86`Zx48QJ z-zDM667H7pR0*Fg;b{{7yoB2%e2#>VmGFEC&yetIBz&@j&z10O2``XvRl@%!;rS9i zPr?^S__Y#VB;nUd_)-a9AmKF<{;Y)8OZfE??w4@!6HTmiorK>Y@i$4hxHZU1*Gss# zmB{dB3Ew8wvq8ddlJFJ@FO=|B2``fHu!I*&c&CIfmhdhKFO~4U67G@kZV4}wa9zTe zNO-S=FO~445?(Ih7I&Zj-z?$D5`K$>r%HI0gr`Y(wS?Owyhg&uO85o|&yet2C491k z*GhP{gx5*9D&bxU&zEqYgfEcrWfEQ_;V($|QVAEgW?5;Cgx@Cd*Gu>c3HM9*?GnCD z!tapqCJA3D;p-)Qm4r7-cv!+WNcd_AZ;|jd65cA|cS?9z!UGcCDdB4+yi3CGlJLC} zzD~lsC0vtmUBd5{@LmZIO88L;|Cxka#6DsWu#ki&OZYt!o+{z@N_d)t|6Iat5`LeA zkCpKMlkf})|AmB4mhk%}JX^wlDdDPw|4PF1C49YvFOcvDB)mw%AC&N=68>umuaWTI zNO--3KP2IP34d6^*Gc$qCA>+(n$znAb934c_=)xg1Iea;Yz zMGb6E#zJy@Qtj7cEXU9D;;%9~8&^cLh#ToXj?Z~F6CH;tB3+DLOmsZaVMZ?`I)UgG zM$aYMN^~=$XA+%AbQ7aXd9!45KUXhNGhY_i5^C@h0({3f>wy`{T{%1hlr-FW2BqWpAdaA z(Orz*PxNr2!;F55=n+J>FnSNsBZ+Qi^e&=ls~BlwbUV?sMU40vy@}{kiLPPvGeoBm zUBu`oh(3+zd`3S)^yx%rGx|ZI&mcO3(f1L3Ceb!V-%a#cM5i+PPNL5y+QR4+M2{xA z_dDu;EzvfjyBU2m(dQ7|#puOEk0CnD=!Ha|OLPmP=MtSxbTgx85`7-gO^nVV`h24O zjJ}fS3y7{^^aP^E5?#dT3yB^_bUvfU5Pc!h*^EA&=!=NXVDxaJFDBZ?=pjVYmMxOX z=y;;X6K!Giu_K@_CA#-pHvU9s5Z%q_Pl(PWx{J~KiJm}on9*+$eHqa$jNU`^M53D+ zy^H8cL^m%!{}#-rmb6~h|y0Fokes$qaPvqN}{tF{UFg(h|Xa2 zeMDbHw2jes6FrsaR7T%P^faO^j9x+X)kODx!^WTJY@)jveKXN^qPrNqnCKj$!;D@? zbS}{?jGjw$9?{K=o=G%qks?iu&LMg_(SAl>N%Rb&YZyI&XqD(9Mqfy@ljwX#k0E*{ z(biP7yuUrV%~(VK|Aj_4XjKSMNa zMIuFveuC)hiOy&ABSbGGI-AiC5`6>F8H~P<=o^W)G5T(z7ZIJx=sStNiD(O>R}fuD zbnn+}{E03ix|`8A6J1Pn7o!&wy_o1QqZblgLUaqG=Mr5?bTgx85=~oyNE4%Th%O`A z&*&?OUP5#YqbCqe3s9tp(H9b3PINw_#}Iup(bpc#e?s)FM0YWIKhd>BhZ+4A(RD<(FnSNsUZR^B zy^Clc(M^nQCwdvtenxL1dO6WGjDCjbdZLRM{RGjs5uMNIM~GfQbT*?OB>HxuGZ=jz z(RUDSWAxobuOvE^(RUKPif9X?R}j6L=-wl2{E7Ax-OcEmiC#l=7o!&weJ9ajMlU2f zKy(YE=MvpObTgx865U926Qgs8UQ4u}(N_|E7tuA0o%qHWbh7vbm;Z;LZAH>&ub=*;OL=7Arwmcrn2NSOnf`=9z z@1{Hy{I_y`%0CSLwxbEE)~>dFaG9Ow4d( zAM$SjhVI9^7DcW@1(CV<)6@j6aZFM43jIG#dF~WSRMw@UfErMq?p2u)P zpW>P^y^RZ*X|u%CYA`)+Vp=OP1q`NJWu`^j%9PPqjmd8cRP9U3|JC52fbRYLt&i!|T`*9QZ+J4+?IQMM^_fivg z_a~@3$aX%|=YHc&_EEJ(y)Mmj6gEo5_)ms7bvF_ru{7_+z(x|LG(1Q42`1hvDhxs{ z=sCJ`o8&*CN#|t6!%_C>b|mlUl2hBRG@ciS>%i1^5ra_87OEyNbt_T6Kg0YF3z1=x z2>r4b#wReflM9XFRXo9{#%1t62!1LAUr%FgsoFNBk$(CW(`d+*@s`N-U}Js&-i?#F zUVr4gq*nt?g{@~p9evhUs8H+JG>CM=3_G&Vbm)D7{oj{}mEY=I10$dnG9I zlu+g$r!qURz8ysoudaH)>P{2Yb@A#JQ+2wm6~W%~HubZZx<_iPaL1P3t{Fs;*Q zT3 zfLZ5*CD?kQb!F~x9{IxHcQO7oj6dsS|Vx1E#hkd3AB@+{78^joXN+bG+xD zY+QIz`rnBS z7nnAB5Z8A0@U>D8t&TpG!_`i1zt|ev2eK2iMf~7p3x9zh>>sTLI^xt<_XpY%)xljTZ`k9DEZUYC zMnz@J9`c2ih~VtjVrCmy!;8 zr2p&lykne!?Q!dds)22B&a8Krf3PC~KCr#UrS-xA?2GI-E!qZtS9%=y;vuxp|8xqCt<~}$oAvjgOg}^>8U}- zqifnKu!Mj}4mvhy*7TsGB`=g6ml@W-?m>O))wpeuYRm{_w4aQofAA><^UqM#Tj(0G3Eo8xJcD;RHA{ETbLKyHTYYoZ>{7c@dY+ z&=z`>Mvoi>+!$811f}7)l_KrOSn%{QSO&liSIC9}`ng2Xs@B$d&YEmMKGwB!1#j9`D-9QM zV%{&~L5%0bE*!@w4Vg^xug{HQ{{a;y`G#O#d(HVuLxdA+6G@9ZIPv1B8R*k*VJmgh z&fQ}wi(oA#c@=|h-jgwETodLWsqq!7fBM>M^XRmULEkX0%hvBXdz=BiNYI+^&;JYQ zHwlR<5t$3qm4=gv*tGMcYn?2NQn6hGlNF%9NvkFcq_l5`4VPIegD))LhE%-%6ecsFE*>CDg_ z8FBjEI2~j1+f^-hGJ1)UVD62LVV`@;cp*KW<|gvOhViHq5q373-U$6L19xn~a9X~6 zJX`tI4&}bK?8*OB8ovRe72Md7ThxiGj@(lE3g#vUjwX1msCZCEZt`G2Sdzb`)H<*| zO#~==p;)KDLEVmwWU(L{l_aBSOT^koQ@fF`uuPFXP)>su`4BX&Nw>&eew{?uFVk|? z81Bfuk*e`tg-+yNicqHQ*q999%g2@b!^(4EEjK0dJ7UGsd0IFSPS$dh*?B&fr6zsb zvEseSX=>86wR_f}~y~Lb44qpOc^NP( z-$oZ&?$XF6T%f1Pk-zW@Yh;7L^IKf-Dda#7aY#8iw>}ay3h%^))WEr{0dM3MDi0+N zgKCkBpk@>I$N4OVQa1X@M@$3c)=I;}qpgU-= zD2$RJ{dBE7m%#G+P+c5V{*2$g&YJFhs7d+zu)rtrYG6lvnTC2g;;B(@FuFeqqS%UJ zD=W1|s$jLg-4N}MIuc*|U<@?@SLwG^Vr=tOiedQVC!!d;h2S7$#jfCzWVNG%h9EAC z96YOQd9oV1DqC$kkf2}lK4w*&wej$()oQRHIq+E=_Hb%w;xaWfGtCtm`9;q#DgFl9 z($v6;REzgKX9@P)T@&TL0}$2U$qpTk)KHlmgMJi&)nRvvX~|Hi4^Uie{lUhq0= zaFDCykW+ibp4Gl`ks6$l>@3;ij{D3WXoqei=Q~P1F>2u8b;&R$x2u}fsjPh( zMXEMGoVU)=seio>o)&baTWNvHQ=W5lHttmJp%Z>L(Bwb_cHX0gQk?G4^=Zz~#Fu)K z%9@;kZE4QHZQQlg4Vu)tT0lf_)ISJZb2<_f$12!*@u{%1`wjR6DX$$wo9E&F8Jb8N9ymstGF0 z7tm3kH5>J{r!hZJvnDK8)G4*eb%WJl4joqeuzzhw8@qfT@ICQB=eO_=k4}y*&YQ3` zgm2h72GLU7F@t8XWovj5HgVePPVJxiqW54MEKxKg$RjBlbmZbB6Z^z9`)NmdOR7t& zOGBBB4xRV`ka6(T)Hfg4essQ5`_4Ykk@;ak=G$mC8J%nluNqDLcLp8F*nZCp-eF6{ zE;~)XjJzUik#+S)&frR$o(>Wh$!giFVoDFYdXgXb5hD$4Alg6&iw|Ymqr}$FqtZvPR8RJP? zhAR&uf+=ISV4g$+kvr&?=Si2cmexC$=I+#Ae~0GCe5)(y?rhwn+(Y-*oL~=p!X~aO zWXH_73P*fBL-=~&B!6S`BV8>ybiiuNqhn+~7}5L|WBtZt5GOFO)^bEm zUkV!|<2)_68jx@Pb7#-+KK~1;arUgvJJNGP*0|le z)=uHh85+4;&8kgawqKnxq3#kKzrf=2EgvlHYJwUNYa9sPI0x-M{G=irrBvOm>Azx1~kbFEw~OChc)=3eI%& zKft+>;+=s5s5DpB2fqDesmw5UalYQGL9CO8*W=I&XI>}FZ#wM}ZSn)2<8MlH zDoYLCK+fVS>7|}Smsdp`5qc;t?GUN*&Ra|s_zqWPA5+496NMBu&w>d8>3@Ew29wk* zc0^u#G0TCLpF(j^4Z_E=y1c1sRy+2EopFeBEPfgFwZd(Vqbh7^i@WGXCX%KgqTzg& zZ-GJ?waenib+mL{>k2*Fa$K?4=R33)F{eYG7P9eJcDnTxbIFxqRr7`Q4R5d`)G%&N z>Ce-35WYUYMP2g^#4XBNx@qlb{8YJT9Z3bJE`S0q?d!}i#p|rm%g<1Qlb#2rW}UqZ zImq_0qUW^P#%k`&dQH_%tNYlwW;?x826EY3&a5|-;TU=m8KWICyVb4+5mvI z(RXOYeW6=Fw`*N?ZKrledOrSTBgSKGUx0sO)nE%PQQ%G`X_eM|?Txn2M#sIe`+E?{ z%5(l2FyK&d=op+zTGMeLISwv_qmtkdtVh<+n&aPp|NW8O=X9Ru$HyDd+SqyGzP3o} zAN_s|LtY#5r+DnXc3Z+~;H?DLxc&NCIIg;8JM{)Or+gmce*~>OI6B_43Fl5w1-WY> z*rM?!l=ur2xjC&-Ob=$wk z-gs~OzsIUu{_!&Z^u;5$w#ymNtjYy07mEi#6fvuDVduCu zd!Q}Dx#k$D?^vfMxV7ENUF3v)8XNwGilA@YfHhD)wGIpEZr{J)jYY5)M(PF!219!* zGYogqNA3NeU7`+74ws!~uSoT|ZbI+M3 z<}X>GRvJqBGb?LpCj^15W`TAt&^VjQahMtMbYo+e36DFV232$=V$~mp27G$z1!QmB z&Qk+DY3h`-M!rn<7emw=bOSy5=8Z98Zt{FIH1#$h`oULuAO1i|btrKJX%U)w6N)+# zEATh}{d`=N;fn62cO({xD^*K$GNu9n=Iso#xnaR{*f{LRRH>8s(&KOWb$Z?rc(R*#iIUUy#7*N|9B>P75-k; z*@^lu#}(HPUPN ze=RFX|HpB~>u(lUs9$4D|3^$z|0A!V{)JNg<4~eki25h;`XAu+4`ZTd;_sF%WdBjP z;`OJBE7bq>YotVi0Q4NH|5F^1as6X>OawkI`G1W1 z&g`pi_Z$7{yN}iP47x7rJFQ=Rp_BHUhBfHHmzc(d?)?Af9c;uDLq4+ zxo69zR4s(wWy8PvAwYD7PtYy~AtMYX1oMwJ?x`P2sFh8R1pnmh)p%$s(G6W9XHZKh zOUB59+dp}sa!)>HThDuH==N0odWb-wd#ADq>f1-Gl^Cwb#S8$)6@ud>=6sxE7IADM zK-)&4^qbwhkpzU)o*q`QMTjkCJ>_B}N$eT`Jssx!$=DK6I9zkEmAV3To^uNSFUd?!pc`>SH?Fkzy%Zc2R89gnK7?>wYj+(u=o zM6`9MYK`;=j)9KkEWBM3TM4nY?&QHyF5@coq?=I`cRxv&DdXFeKo0NI#hq&4>v(1D z-$ANDhhLqd>Plb{QBV_VHzOj(XYSGOCN~T%!U+Iw@nXn^u%1k+JQXVqVlVTNkN<&L z5cD)Vv9okdIq$#@a{bp(7j{Hr#r!{M8YIXZ_Er>F*!ezYj{ep^>DKGKbZq2S`nMqC zvFO*)7-RXsa|XU0sNy{AcS0vwqw=(1MCBjsCr@jie(HeoJ4YGq_mf}PPktK7ze!z` z+D~WguY!Dk-N_th7~)KILLQK$JE8sgV!g{_@^ah6{WeU;y1#`l+xztEW5^F4Q2y6Z zc~YHfJWZA-=geoGvw%4%94D~|YrFmdrgfPzb}twb=Kz2|Ka{~3uV(^!I~W|n&^+er zsiG|PyoDv`@yn^dax>nY1$6x1WBjI#z ziYA;}qY0;xzp2_kDc@AvK4bZ&s`WaLe1M6GLq}j%wR}haeA9Vkzm#t>E|!N1oTZQ_ z=9@Sw?w@Z`(rVbN$fo46fK>82g&rmB6R+*Jaj^rf5F6g5!aV6Fl3O;JPR9{W0M zn0EW#QiJ!CQG;ySyF;FAHR$$ZRPJY^qNL*C-{Z%(&~8t~K_*TTPWf;I+om9rwkDw& z{b##q{W_q(_Oe*V+Ff`uBIy-6?U*r{1;LxJg!K=CcK-`(1A##_VkXl-X-Fhqctaz1 zn+1)eh hXO$hv?yPSqY7*piiSuuAeVa4nN+0Xe4nuPmdl4jY$*R!X(rKS6Z7v^sJfo+9Am;l}w#!z;uadX^lM*5|Q1(Jr4; zbO?w`ss$(5I6h&Qt{@A0uHdBawvx+zn&!^>x6(KPwW5gTTon77G=uf#ksY$SVRr8= zYN*bphF%~(SLl|pdTS?Y4?RmXVA4W=43JBEiaZ@PQuD4Um42ns&3M@K1}pWG4y_9D zbZ2AVuD%iXn_y#7P*?%aOU(|J+LHC@Ur-UU2-J~* z@}5hg@iBDLXTxYcXJ(X^W$fwnyLM1j1;{8^DNicmX;iVOV+ee0Se7p|@{%XzdaN)6 zOD(9Lt4;N=8!yqHr=bfzOI(1) z(ia|b{TMFm4BU}E*5X}az@bT%DM*Pax7Bx`+-QLXGn@y)sc}n1Px$8e@rJMuw+?x2 zp-ESf7{Y`_zn9Gf1HXiDwn#5$ICljcN4T=SR2mb|Q2!Z-hm~e9rmf`5Y4CekYJpK8Jg@$mhs^C)3)-JeD~!{oX%0 zGVbg0pnxR%`cBOHCT4~X$9nn}5usUN8`!sTF8cy8{BWAw^#9NN*oX)9xaF)|h}FRJG1tAFF*{K{de5qk!Jn@cm|*O`{bGL#qMIC zek;08TZ0F%3OYk8Q}y)-LzF0XQP)Dx&O!a8%OBBP#%(}<+Q7?%=qbE6i#Tmw8n4IKtRXQ&4oDKxe5PHzK_-= zu-?fJly1U_&&hXFdaL_hAU91t>8UC4l;Yyp<}t+kLoVgwqtt{e>zkmTwpdytJ=oeh z9oi{(vZt-lI9FmW?qP;8kd187%EAqi`)k0fG|=P9QeqrrFz!=KS+PL5%Z0oRNq9Ey zU7@e{+o*?GU8rR#ll=vjijKxST%$!WvU4Rvb~jQNNj5DiTf}5%GuiAuS%MAOYx~Ql zMr9W;S$asfqw%UfS%MAOw`rpE>C6U~=}Zfd($J1Bb+8mv=uEI7Thw1R14XRA4nH0e zWrBAA;)3)2hSV3V^M;k?H*t~mN<$7apJRQ8gN5Kee{y6Vc1@{=fi;M_f=glGFbwQy zrggf(LM=I-q-MmvIP-1yl$8sXU*!(^vazwxaMKg}ufRyree){pn7`LwgCutBS=$uX zUj1`;4K^8gp~1Hg50hjqdJ^+faW8sOTlA#92ZibsG^oAk`<-2T&mPzjmzU++paxg2 z&r{sK4SAYxgZ?CO1<@nC$bnudjk)N4p4N-|y;+T93b6YXH@no>U;Q>F!XhiA{7mW< z_8y4VlKy5}5dPOq}?bIE_28Y{pkJ4DhWQhr~%AeWn4uq%s zU!YI}C!fDgyOZu~D~%(WUpVoRwz4t;r@jvkApR){)Ge1P16moS>oQDe=&fZREhTLm!Epxb9X(*NO|Jn!1@&7ERc1l~)5#EpY-eUi#6jzlwimAp z4se?DBF>fMr#JEZ10Jo!9a}u{ainh%qL`v)EzeNyyp_e783^$m2=Q!Ys9AU*!hsmX z;+>-zyyP(JjhHo(i5M~KehPwcSNJ`7o6>M6iaQ#;xaNZQ6Ku#n&_6n}AtqUhl2UeXp1~TisqA}QcX(3RwlKyPpZj~a`l%g zib`!j949Hqqe8t6hR8u?Aj2SdE{64}6*tA1-i}t|oSDx1V#V(;i{JE)Nr&TQYT<|C z_nT1C6u(EpW1{i<(pFRaewoMbm$gMN>%&oq_>DKt`o-@~1msqV-<^oxo%+GQgDZ&6 zXpUAEjimVPcoe)F6u0A1&GD$-&N%r|lIGaJnBRpqcsGy5EwVEBn-H|^L?%c~OptsCg0#HQC69^v>0FTc zoUy$85@CR!7%@TeTFmKsd_k5djs4Z%dyWdYuX9Id?VioViI2n`&1iHz%+ieISpO4= zWbw6xrc+}pg=9xFI1$8X21-G!Z++)+jOM?HW5u}S@u?wtMGpmV?2YJEBDCPx#OPJI zanMDND%YMik7KBvH>JWDrU#q-`2 zP5Pb(oBj{kTH~Gc0H<)|W8TcGK z@7!cO--j1-XzTD98(XX+K?iO}vIi~x^aM^+>Cwx^&(RFh9=CQtX{>}r&{O#dG#mwB zUh2316}de<$sH=}rKX`cQsi2wT9dInrN<-Y8(t^Ji5_idRB;$6250rMr=Xd7 z+B7<%Yzo~a!4WKqT@xIAwq6|GXo;YLRus zdoH$lOa6%Un7?1(3Z6@MK-^hhAr)X}Jn#B?U`cwlo4?u1kMo~L>u{ZS(BM9HKj<0g z_)nuX{=PN&4||wcyi&{FOJCp$E>Ct%S)Qsiv|_+A!|ukzUObGu43F6!(ck?Gjm~sD zhd-UU6FWp_9e4pvDPkDG$uhCC&J+OU&Jb|Ow6oLu+ladVNo-QPwb07BtKFd&M#&1=rXThyQtuR?=7mp9mFHfS>Jjuz)vYm z`vcAd**tOvapn>DggrlBf(Ovi-~rz!jJNSaFP@b?pcns{`r%lQN4jBAj8|K?)fu;g zp71tV6;@+s=)!bJyYP7Ddb~tpjl2pU=J&%_pc}}|G*(J-P4@559aKQ(KB7NC_(u%iroVtF#j-BEzfCNG zmCY9Kcs1y2Cf3zpC3i*lj_7wnM1M-iH8Qz<`ls}bMN_{6>08gMl=a;#ita>xF3s26 z^N8dTS-X_G-p1;JbOBEV;*@SZ4X)DgAux8rzyiV?tTA*}Q4Q3X;U%zc#-q~>Coeji39;Pvq^xBl3D^84m znAtRYU~c^f<6k!ox{Gn+F}R>Z7ZG~!V`4`6&ce9cFq>%k!7ow3IPS0gk}pf*!EK!HINJ_hUEqwvEl2wG0u}FTY*2Tv+@Mau zPF0(ak$10GLN8FZryapMaY{!5eQ``1i-N9_!M@b*y z4B}Z`*OciQZe{xWE@k>r@@f z&CbAQ@%TXmwN1a;71yioK1!KH!?ze+r}l=k?PE-Z_uyCSd03y{r0;){?Cf3-JLBfG zJ=vM{Iz_jY*Q=qKXy9e)s59kDc0^tbVAB zHk@&Y&xa|l{Dn>P>A0=_kyD$Idm4YD6 zM`tK69U~Y0?JV?0|ON)rb!u^H$0940j!(=Ltu!SNLjcJ zbcLe&P@yH6fyLnnbXva}T&RNm`Z;O71z6o^PidInWbF zG0&Ct34cc#D`l!PoYxnb*LJm%22>KD@7 z#T#6%;3WrsHwHFl75mOXWcK|WsRK2Krh0rgn#`|;SSVbtPe$ii7+k*^Pcq{NpRiqZ zp}n5xqb#RHS(JNcbJH-k^S~CF$^aPebGS1m-LH5h`}9FniDuY(;}c|Kbg+dkVDwh< z7kVC`Z@H)E%~`%k6Z3Tzb;Ps7s=fx}hkhu5E@tNU+~Yo`b+hMW7T0->p{)HY`57K+ zV^hiqO3S*bVT#_{|47!N7pm~%8v9_tR{i~_L>%igcy6-(YsP38{1A@O=!Hz!dlL?r zxxJ?@Z#L|G%;)AKOU6H@G_btUuW7W7 zX@Oi1p1|Podf`{lhs=5nUBIkty)?`Ud-n1lwc}ZoFlpf6WZX-J%hJ#AsG*yBqxpca z({DDMV5g0k2HdJq$Aq2g!06Q8HGC5p6f>2$+0QJt4l4@YP=tq0a{LzYAn-Zz!Xv&5 zfwdR})ak(sXy1)i?!Apn6xcpliWE-l67-*ArGj_i{UG0;DGvP+E%$6@May{6W~1mH zDtgMW(BvdLp*i{XS02U zYn;xWJ;2*-6hYg1@C$(3Xahr&zwHC7E3OBxF=g7YU0HiRRhQMP+=XAIz{Mfu zF8tV(1*fg4NY9LYI=u{u*A+9CjiwNZdks8aLja^)mTthlI{#*f3 zw8}!X_7Lu@*5R=am>v&2?tI)BKei!ZL-P?$MPldM4i+f&E(X#;oTHQFW>ZcHJp@nJ zauLV7NO`uYLVkyB3+pR-7@3J-V=KflTE8Eq$*Of@86gWXJFXgojhfO(zZJojzt0iR z^%U%KIi+7ifvo?yGvj^OuSnXRe;h2D7CDi^W9v?cfx}hR0dVCvMirT%8 zu%+aE%oq6U{jlv;^1824dCy%&Uc?6FGiprFpjOFc`ugJ@pv5ej_CFJ5(7$=~1fevh zk~El5(+Vtd7=6PVr;$doqzD`5_Tm~^T}BHFk5n7<@l0hzvJJ|;r!rjwx z=n401>3Lpyo~q~1Qt)Q$V`ysEH6>S<`mxV>U{uo=oI^X^fl=jlt$dpo(WRC z?|Xk5uZ!ZvW{tl?jWjIV6}$lNW#dO!b|UGE@zS#&Aq>Bc_MRPSK-&kYuDB1>l7x=D z1S{aPg9(AKRZCFU3t(g*!I}q;(cEFZr597VWD4c+=i$yua754@Mw%07$2&DMz}2gb zphuEQzE(Q5yb;#C;1qkH1Nano$#MNl2nR=iJrA>l_{;2Voynn`0)*JQzRPCgQVTTU64k87mwp!}2(FVYxGwmNEE(89_^JH5g;+}x zGLMt*PIt|<1QSB}e^po%P{= zK%>FLTYibRPS}_^@c$g zZLY@`81!Kmp%bCaDs^Jl!xUIHr_uEmT+_MRcx=ymTFuX6V$$Q#M*O~Ma5U{SajK9; z&3#u&R@tX7LbJ{eYdSM+^k+?3yeUaiH#)1zs%tBYz3SHT>MC3D;%c9>)oYvUDXFe4 zb#5%LT2f~#t}11zrE`<5z~l7-b8cSh_10W@>7~n^Tk|}1fQr31sN7O&T+esD;H|5v z_Fj5(8PcZbJ>yHND=%GIUC@!2L@pkuZeG58e06PcNkuRT0}}`*@v#_XwWMfC^iy)Z z>ZX~~=i1yJZ!vze#@dmcQd;aS4vq@tqy*ZBAwaOI#%ZGTtv|nV@aY0`l3$3cQFvY%^W{*o- zjMJH$&)m#jKFvw5y`;RVtRrV^S+(=|QeSPc7xvnbbK#}IB)~e!4 zPjFbw;@Qz+Cd$nKZmKC~2aP|7L3quWRcL@I8fwNbq_pvN`cu>@X_psFvWHZwow>_S zED<*j!5UY=hZFLvd+X~88 zc$gULaBi$4H=b8s z>C&9m9rhGhybQht{=tDZD76!Vg5DeaF6|m?S%*EP(qm`OC9>ve44p$5FSj7b6p$DN z*b~G_fwp0y0~{P=XYYgBm7RH->&M5#r2jmjk4r1XxCDo791cU2we3uHgo=~#-Y!)6 z5Nf;5$Dj>^9?rNg+*&z)W#ZNp44i#rbrq{{cz=yY${L>%Xd7kLm?H5{)i^JB`9@ko zxmIOxr5$jes8*M|OAb5Z4%<7XTPus}3!v5Xy87hXqMphPgq3fCsjxa0vq*WPhG=0=x3^bgh)t z0Unr#YR#bui&`bbn5O84ojqp?C))+{t`Z$NPO43VeE*KvSx1(j57Qi>-WkdECH5UNsMD9Da`gFlSx!gZG}fn8gF&KCpiqV%3EDA~ zw`ahErn7pXB7WDZ$ifRb~y862>}1OF=}EeA0b zUDK?gnS+gR*I~Ch7^9sS?Tm3I7&( zj-zL`!Nqpuq%ajxH8V~>26f{+F`=EUEF*=&l;uT* zWIah-=!q23Fzz6xFZ-c4W#>=R)<}^=w2ennL^+_cE4rG))&Oa{LYY$NHl$vdMu@zj zYm(Hh(<{oCEJY5TxziXBnoGzJ9DQGTJpySI*HwY`#zuzW{S zbSRyzwW8zd)+Ke9G6FRrPhFAHku#D;FgPkNG*ezBXPzQ0nKQAJL|6Qor{edvm{L)Y zO$JknY6*^Nr4(4{G)%##Oe-E?Mvps@SFp)}N}#PYpw5j;#1;XWGkQETWk=4)C6vUT zY|QnLIwBB0Wz(6ci3ShmgW=#3l=O#2CK-K0K0mS}=SeV3yH zVH>N1=fH7!T4|EioN1qsi(3LoR_UzNbROf}tX6vqu#2&AKP)b_RoU6wBF>E-5Ho74 zeSBlI)!5+BCdk;?;2i!1W1B>qplY@wvdt_|H_xG+kH@B}B}rnN5RYk^!vc=j*@%!u zI~yKnW?t*ujJ;M#ZF!A%N6s)e2@EAa7XrikoNXaYzc@wRTv1(I>Z#4GKp+@-Ed-p; z=T;Qg)w!yRh5HH7F;UT0v1?n#$FDZUxuvqYw7jg`Q;W$u68FN-bZ!+u9jZ;Q#QrA` z9wY58Gc&^*B;766!@QKb>8 z45fz!!pi?UN@JVZx3Q+UCydyUZKWistRp+QxU`iHhbSoF_-zHx1(0OmBu?Err@%(B zx7e_~R&S*}xXp>(R4AXVb%xQg+8O#Q^u{Hpl1P&MH%F9Rw0{haqMc~}fMQT0(vm8a z(DtP!Sg`tXla3tg5=^-rImtDfC=vyy@qOwHByh-4jeV*}CY-G^iC#WVynlB>LoEeXjU??8XHd=nevF?Q5=h^uc$J!5d7V~EHX`8GR=sr_U7 z{b|yEU(%NL`=qU$&BiD;vRUjNXb7U((Y74fEZgtTkg{104*hA_tjVZx{f)}n>QfY_ zPbF-xUoVab#3*BbLdvi+f(6EApKtMLjQVf!XUJRp8N&h{Nj*5p%U148|tS+i96`1z9ToK&c2xvPn<_!9AAP!MfzZS zU^}e#DR%!ij)%ngkaos><$S?jQsSwrvsKcs;NqO+j`S3EjDcTN=(nZ6BOPZh(LMbg z=_63zNM4PBCm_#*GQsmvBw82%;pZMqIf+l8iK6ZQhq^a`ud2Epe{(ZH8016|jdN6N zp-$jP3TQ6ez`1gx@I$IrQEUWA%IWb+Y_+AWZLzH_wY8FlVBad zI?zGE`COxbqD(4z-?jERcSvq1{k{KtpZEBD$USGDJ*>U&j^jDMvgL~ync1tHZ<$QdCudxH%e0#(Uw5g29uq7BdMOw{-r~(Dn@Y$Zc;Z?$KG3VC8=<2|l<*m1SL3s6<0$Jh`I3$zd#VLt4 zQ@`mYed}8e_qBpaEXbBS*>7D)A^fWW=q;lQZ@BTg>%<_KJXN7K27w5x>}+<2Y}r6! z5Tt?3FbMp_Y_PK5(m-}l3h70(D86%TAGl%!B z9T^vT*87HA!%xYYW%KAh*Wodl1(~F*uvwxTj7Mo!%B$`t6y@S3h@q$&CQS+8Q!52KwtPR{4?EdTp??rVpD47g8CMu zNQV)KuBh>;F6PWDq*Ig2Zk!r@T3teyPQR9JmRyJ9>ady8K`XO^VsrW(Jgk0AUxPxE zgtt*T5Wt@Omg)%=W_lTP#wJpxX4W(uQ0k!YeG$HPR?FFUMB$T|j;txGnPoe()FmZ_ z60zwhZ@fbP^YW~aWkeoW4fxLgdhtVf+}2RnHk2( zvWLl#iAc%nBeFL(MJ6J-n(82cyg(;o0ZFL;w!#dRl9F2} zc231YJ0iS7*7>mwHWX(nCp>i-l3ztRQ&XAYg`bp-21x&s>!hFIHP1;$521F;bX2rW zVCB)tLYWterN;!40^~)TXLeNZljt;% zorhU_g{ZBR3cdn?6ezJPpjmwepI2Dn6{0#Y?Hs99vFaXQzphYU9h7$Z&FFJv3J}dM zRHIIep6j4;AEK6#g~*PY=8?q;_Uxkib!M?eRy2Z4(WiAHeq9;Yqs%S@YD$>wbTPFs zLlfY_H{y7)*wv{u1c`-V0v`$FFGfVG1C%po<9`A?`0jTl)TwiaN6?vxYZw)A=lcje zG7>B**{jgQH{5z%+1RLaO`*MN`mIxLycwS!YfOiijlcF5Qf2LLbm_F4S#Uay)x<%W7_rTBsYJ+>Ejr^6hY`^H}yIG><;suk#q{ z#C7OHSejAiZMU2`4YzRVG#1@faM-wVUr_80K%Rk+7S-a1t=Alxvz z_q9{OK%W|gyjT4?rc%&LHf>P`GkWTb6m8*>5PI!R_N*QhOJe~oj-H-!>fxg4hNm@g zx!J`5VTHrc_zJNQ4v^>g#K>+ifua3H;y&o6N9|-E8AMBYtvlk>NTa^3 zS98PkjOMYz1E(@S?J7>hdqKVbap z?Nw?0t$sM%VAHU;M)$67ImB3zKCZ;v5y+Tzrg3$%;%c=Z;@a!R*Cb||p$b&4xZXoM zqNa^YXHAQNEWE2YD-$z&)HkVeR%^m!3~-4Fae+H=V`6kqOzaFG@|TN)qyrGnRloqi zg(4?(cOR(tEks>b5Mx}-L@+WMKPhIIEHhlwiw-jXg=!i8pg`1^fh@v_afvA=u06^m zMju+=a##i^g&Mci2jzn0x{A{p7`O&WnaB{yZ2E|oROXb;6~!|I3q|!!W*Jhvkv=NR%!-e;BAxw1_%~9gN2w7 zo65#&r_iTl=t+%Y*k@%h9Hkk?lA)J^Z+yQrN!1+|S7g6xhx4%Cnbz?4U zwi@JK;B*2<@E2$0;Nbf6SveM14HBL*U4%pDzXO1nJFT>e^Jr72G*-EDd2Fv4lG=xU zd#UW)J8iaXH@>7S>RwYqfL=~HylKUMC1vH*(iZQ@+u7~MiIW$j?l;JEHJRin#9LPU z9nG}c`-#a>QxtXHXybGeM=RXR*+O1XCg&IHw{a%mJuChbg>TMs@7sR%-s}!s&M_Lj z)Sglca+Mmc{hqVHJJ&BYj8Y4vREZoWNX+8QU&7|myLp~Di=l7U!VC0s?TZpz;tI}j zCr_6IBFg4!#o%sEqxa%%+S!`vw9+W8{CK~#BFAYS&{n$nt>~Mz@JszHt-R*#1j6Kp zlED45xmr2QU|@-NquQY$`BO3Ys`_`Q;& zU&l$WA|z;z?tTu5#=`FR7bVB%z?FPT65PtgoqJGQ0St@1T3~>TtcEuvC&x_%`lEnF z>;2Ol)3SsM2`K#86&@y|15oV<~Z+O@4#LqZ3Q z6ACFVZ?YP$)Wm@8;n0Cu35bc^I$}W%1j92~2?z!`)e|uFEmiJRSo))yi{zXXCvla( zznaD0?|X7CNqAch*6f&}g8*CCZsmY9Y1_Sjn>69x*V|SKgUg^cr&Hf(`%!9S*--xn z|BJA%4x*lj3P_#|Zbk;B?xBYB5P48H8E(qRuvEyPhsj{X1{tCm8B{{NCZpn3HU%{d z$jE>Q-JE>bWB}i$iS8x?AhufZ8S*jv_=)Z>&OD#Me*55lH;x?Y{cePP+!b}653@y| zds-(wJ%}IgDFi-!P&R8RtKpKLZQeyR#$8s$&d320cbM}fpK#zK0=}D|plE%h|9}uc z5<7USISi0_wOjhUc49PVd)C%~?CE8;%l$%Ea|7HwkH}ndEYKSDckfyVk88f_v{ z)KN;6AtHhl};KhbcDy=z3 zHqM_6LSaoBlE~%A@;vRGq6CHDG^a;0d5H`LM|rh&8I@fW4jRw^5S(8Vg5bg;Jveg< zc-<-Y56Y1mOb8)YMq`>?-mHh$XT0XJVYWTj0KZx$8se+7|Wjd6cm$`(#D$vp_gultV zA=R{iT6;`J(+htsP0!z}W7pGZMSnT>@e-|{S&9j>*B86aKg}5|FEI_FE;6x0nF~k) ztTL;iuMgHva&nN&fVJCpFYW}^CKx}B!P?U3z~`d#Iri2*Z{DFiC`e@yXDcjtR3o7n zMBL7yR%gQU!KeBd1EJc7fnAUFonfkB3% zRYF{kgm@d&sf4&tAreUb9YAt-014~NKw>#$;mF2bq?DZJ-0sbmhDOkk87DqTNGUzB z=3OJ-z9Qd%U(!txTaqn>O&~UjXG`Kf(Xc_b6D36~6cH&#p7_zHpkTRZ>v|O_L%5HT ztJ}D?Uatjqk8kUz=cz}OgvAcVif)oKBWq*sh_YDuy6N0!HlnnW;9JN7kJ|+Gl=#Nl zukh{mA;O{AY@T`ClzO8>CzRIA79BSo%@xB+rQB_;8V+v*tqmps)753xkD8pyKRjJQ zg7+*K<4o;V1ZB+$QI{wjV-Z~3!0C(~z%SBpEQcj81H`dI+&M(Zd(&tDZw+G|t*;d6 zKT&`RC8r4P%ipTG)gJoIGVA;6tSA0u+L+?E@gExyIid~d8Rx~`M5WS7q6yGWN`mZX zY+5S+AlOTuPn#M{sO|pP(CbS%xLLEqc8^c)C66!bgYi)%nBaz1l{u5QrtUtSvj<@_ zDO!$svyxZudbtgpF)Y=t$jMIsI_uru!kg&t(3xe{UHycsgeR?r zPk1!Fo+j!C%yMO94JfV5=IHf^3X#?DSIsbCiT5%R*&lT|Hfi9qoskM3 zzX~6}3Ln1)zBdeTEr_&~;!j@92s9f0JrtD=&Oj}3ulp6 z;pR%(s`C8`RhT=jIoi0yt4PDd;lzW3$;1?hpkvO;0&0V@FYOQgZke`1q=hArr1vkO zC^IPOjvr`XtS7Kg*iF3O+Xz-uW1^vK)A3{m8%mFsH(U3w6Jx=f{@*raT0vvnkkw8* z62?0vUDBlo$346c&=uBTTFa95P{U0kju|ol6VxV;A->Y&*roMbMJOn}|!HQhWYtQ&R{Q zYi9#1VantKcP4+BSA#vHpQF{s_V*5b0!KUHdE`K?o6t<$kE|zLdxRar_Hbrx5gQhO z!VOuiVJC7pm$jx~agSmickfNwMBJc4TWX0v>3790(&Lj(d@%|Be8knS1tY$QxsNN3 z+=MCnGs%RH&jkOm-T89#kf{wf(>B%iYa1M#tCoZK73hg3tnQ&Vl-;(?pIp^`Y?o^t zHQi!Ezg;%tBD$(ICkV(PYV4M!{+%x^l}`$y{hMDH0Xf~+QYm^jZ;Hm|YcV1ByzLq) zXPsR=dw`s5KbsS-pwqy6r=0nO|HE!KZrOP2^ZI=Vyq}Xpt>Ld6O-Rh9-t~Jz)?I&Q zan5#&zI8QZ(h|P`&X$(!gb9(C^HvF@K}%fXbZJh`ZemP2@vVX(d3mwDF%ORjYP{ik zjxSM_NP5m*pR!#}$Z9-PTotV(dNXK{TM!J;itCuW^m1Ug=dY2IusBXOk`MQg)zGI@ zAl1uVZ0Bc7NRGL)uy+zoQoXAgxi&)Hh)hNRH9mQ~w4IoqzxXy-H?hHHomb`F#&mR7 z&2X_)M8>&UhRP373=Hop5~*h3KYTlK?y8vcxKvBw5(?Kc;@Y%buB{O|j;SjVs0Bc) zVbYNTbX*EB6ndYW0_IJ=IUAjE1AJ6CBTti%_pzamN?|Mh!Z8AnVxJ*- z=dIL`c<-b5Qv{?#5_4A$kXnn%bSUD|C5fWY(Cw(qp;$yRc1O&4cmYo__dCI$Ff*-k zK0Ofr=czlOJc`0m%bFp$9n8E3T3+>zxG}*0$7y3)@c((0I}hBq*JAYlOgapP6*j`E zl>4~;nLx!=0RmEuojn9%`iRl&&ZVv0Is}hEh~V>>DrZO4z&+9Vdj$RSzQY4VUUvtl zG@788Yh#I97N@T3*zSE=Ep7XvR;hv^Z81@hVO>3ayqKD~DSa_4BA{js#vw2wV>Xt? zQgobqbSt_V-9cFuHtN6xqx;#`^yv1OtY{KWgT-W6m8tqGeFaP<05Xh9rcR<&^TJ|o zVu?OJDk%^+VFe|eP(^A^(}ABDPvNJxl%?i@{<>2J)L9$GhJ;b2JI?rXvMPC5d#gc+U6r=B`#7GD_Lh;uQ1E$Eu;y$R zs~vGWO*Fpyz*cG@6GjF<`M7(7FIov-M7n2{L3LDBpVr^|^&eScA2X~fL`4ju*pQnL zHC*1)TIj%O$(MJF3WlECeh*!Ck|-%VF@m{@5qM6#1i-C^1zf)quA7s0jn(iTibdvy zeF?`^zo%d>hg%3OSCpmU@GnVsPYtGJFwqmx=xYKR%* z>mwv$ou@n_0k^zYsNL@w1m&|%wssQAR7P4R4Y^zBHVxbAn}*I{YV|7mkz*3KQq*zYFT)xJT9QZQkilQHy6GjceQ}i zK_~s#G=f0P&`0xbMKJ{eQB>$P2t)0%+_d%H)zUy94YmEmgu0eWiJVFoTQo4kebfuj z)cF=e7RfI%ByVu2n#`x_u9Tg$5hy$9Lq}zIlQ3f9aUK1{>OnH5VFl7RlX7Y~!INsa z&76>6NTrxRq@A>!H0o=)oa30K-+blvDe6bZ@jS$f8OH$_{hIP|s`Lo@=!BjvDSmjQo^_n@tU>;i{Z%ma~aZzJ}BO zBR|i+GBw6IHQd`Yl1syti(zlj=%ea?q2XSHzZeAJPGe!PpKM58C_^MTV}@DrJA@6< zRLODJ#J-=_E&A~#63gw~{dTp?G#1>zG$w2{2AVWh&^m26)PXM?o(kI9uK->rMUs|EWV zGd6R+AyoOl6~R}l5DhHSX|JX&(%Zi&xIZXKJVGa_0qUiTMiLucrt^&y>JNUt(NP}% z=Bfby{~LKMP=S8|N#`4aG+(vmkjGy)ZT*+?jfTs#PO(HV=Tum9nr{rEW?|V}68S3j zXQxQ`|Aa)2YK6h*#$Qg4?vF1Q7(2<|o7q6*L-7B&{CyPy`ww=~0p;(xJq{#)XPVkO z%HQj-#Z&TkC%g62@;AySU;c^^5&QM86H)`3Q~p-dNG|ysWwMeU$X4j#e<6R1(GEuB zGN*v=jg)QM2}q7kCK@|HTa<=27x}4u_h%$(2Kga%UmPQn`Xer=eFmB|UWgjb#;4&I z-c8=m8fai$-W?GK&CEL6J>))0qhh33@;j2nc`^CNPefmCPZonk`K?1S_?99oF0Ni_ z3hNmT%*WW5iBEguOzZ)-?D317uSFCo%pp=4 zrS3eiwNf3uFhB0Xg|5r!*ZWKb{Kj@TJRZ zt`X~=&miE4!CD;!z=VnD{hGb zN+d+t41US%W#}FwNt~C>R>KYk;i&T5mlyqE-2uEjK;_Oam}e+CY>i zFQ92$xykd;8}fU`2U1!UVX|Qtm77JXo5ABBx3jCqTt#6TS{(4!h z&Di6L4)KS|)A3~?*`k!*<9P-|3sEi12OS_vc8-dzaY77Owh0o0wY7EL-@s+t6Q(l3 zp0`qvOJ!>rd9CbCYL&`ffbBCXdx?C>l_DkN=FlhoOqXY8dQ>t+L|#Y$>~-GjkWt$M zZH*cYeSu%*r#N;~6}Nk{q|DuDFz3eKsllm=QlZITYMvzMtCReI@y&6~Daqd(bCXdoR1O z2QQ+Xm!vUm;Ecdul>Bao^56E$U;bVeuvba>2x;1*QGTYk-p$H%zGQlVG>xRK_C)gN z4)yo)iyr+>R?#n_lMx$Xs+!8 zZR!}mmiOASYUwStyhNI2+JzdE{!Yuz^uAPgRgh_upXoQBWYx4*GJT&kt?3VbrpuC9 znSLgjMv|tP?)EeNU`JM_nUZOqq$!IJk{pAA&_Fd;~5oCXj+O!VGuOsed)$v`a<2$5f?B+`Cw|jd0rQu5}uNfbc*k9)P ze|}4t5)t>rTz33>o}X@F8O_AYVFfEVuH`2Xbjm*}#62zD zyr{c`x&*bZZz?tctNRytze#_HJ{g@_GkN+nIqvp`pU02gerCsD?%jfb*v-Vl z4iPa}PdNGEv3p7FL}M!;j2NeYhygj7;8oX25e|b9>#1*HJ~5!`oBHs^nOQ8H-dZHI zIp*9@*izB^MuI~~c8L=u*?ByK&{nDJ-Gdg0=@gxMvxX|1N+U#aBr9jFv{YeYEYXxX zr0zG;&2NO$Sr-$3Sx!jZHdzXA^i&HJQMSIR2UfkzfK%lP{Bjg19C>PUMYmI>P=Q}4 zBTklfC8L!wlcy8&xuSUT&4NGu;3M>;jtI?{DrNc5-~*I0ew=mm-}?j|jycl{!RW;o zhev?fl31mbOXCr|mee;LmaE)%rCep@*r=4dcyxV1A8*A!rI!WRA=+QpS*#RQfa#Z4 z=Js_o+?GV*mjh^hxfn=q4ka3Y%w60_RyK)8=RZ@&lR0QQlnJ!euuf)}i7~_&+#GQ} zXMLD$!gW&`qt$oxA7EGiy()3ek=~j22|bP~taQ(p^wWs-T3qRzPvp_6(0=dG?5~5e zzwT_zDtk`)tGAg?VfRStkjn9iNBR%Z=w#E26W@gPcCpzUJ0(`Va|)ZL&Sy(i&#-fX z?xpHpm^b$nzlXM4TL+|E={5LYzWMSE7(Lvg;qzrT;O!Blbh$_5bt`@{v@)`V+Q3@R zu-lzI8a_sKZ&-JGQ8aOB_d<8R>{0=f`Kea)#?^>>vhCJQ;+s8R{qBUU;Z*L)*ol*b z3iXas&X4^7b1zlW3|L}3NH%v}7Ri1KHV&1^kAE?;%aJWd_EVen1!8V-eL=BZ-(2PujsO@}L5{hPm`xNPSq&R>)>*X} z@B9t4gsmtI%8;9Nfxoq#VAd^yZO&{b*~;qa*;Vc>Y=HTw<~;6UD3g6)Y(QeS1Y0L? zh`i?Y8zu6%_LZuEU-&zv=dQ-4rWCthKB|2-R_-tMYqK@6sC!$P%~hhZ4?J4V86$S< z>58=0)>0|8F)SkaWv9#D=)evBMx%L`1gV{4K9}+v0F`d zL9o2fp5taJ=58J;5uUGfK4a(?$bJUpccMx~L$7-k3F*+#DlTO!(_I#c zONyC;Ilasfc{amhSL@(}6PLl0&j{Co?kP^B7uR?og6zZ{eG8-2uXw|t!$`rYVLW7D zCE}c_J1~p@gKVo`sT}-~&$JIQNCpeSv+Jahp?cA*^6ix}s)nF#uqOYSPT+<|lG`Kh z%EReqw7La77MD+Uc`G~k_j$uFkW#83A{_g1n&CF3cynWj_bz+nWA3>}oX1Q)F?W#n zuuIw0ttj2yz7c1SiDM|e60yp?D3xq1yEjQII1Qz;u-u!fBDAXRlEfWPa!8rmbx)`h zK)4R)2H70SESG(x%%Gx)izl@mf;qym7PJ4dPx;ugIopHnm!CuvqbAwqYpjM%_tK?v{ z+_RD%px3;gQk=>{`^;V$r?0iJPQXO0#fDB^V8y>q^>+Pg>1bX}Z;&ZZ-r8KVYzfl#dpoTsC!5MG9nvrp^UeP7P(r6fRABrff^t!n7cb6BKGM4 zZ^S8_-4c1lJ=cj_mU=%K4h2CW$>a9xqHzNqr+%ZojcyBeDa`}v2Cn2)WcH(JB=_o*%v0ArASIOsXj{nR?Th#$$1k%j!aNm zXcD>m3mKm1Fbw7=1O`I5m3U|QS&|u9z9LyRYnHi##1gMsKo^nRrPbKhA^$=gJMq^-UJS0i;^1?B=I4AazX>k+@!nODNDT#7JRT zyJit56j{y-vJ_`#Db_4%HRXAIgDgEVv-Hp`#io`W7y4-HnVF@hW??~68hSp+(knAd zFU`XGs$_X2$kIDAOK;7>+M8s#JIHcKW|l)V3+wHY<+>nCpUfTM{-a7(3*LW;Y5A(v*iSYlX#7rG0Hh){A1^z@29 zH`7n=p`6U8m0{|a7+(Hozp6e=5bVOlG@{dPzyKx&L$AVw-EHLzKevg?AZf)bXLU;J zxpHc!wB9SLJEiqmd3vX`l9kgsr5(0%dWW)I{Y6+t#HR&*1gN&77Kqc@=q=0>@=0HJUT+6*>qF;vi z*%-)nmiICT^0=-)<)=RR#G~aR19!;Lk4oA+`@q!HD@9%k>tCToLo(pyn$=PYXeGX*j*U8=Rn@vU?%b&M9bQFo2!b8O<$6yRVO zeQtfrdCpLRw<*GhDj&$FJnx-5gYIN_Hej0PQt1ffY>XkbjllHgp#;q1;9W*G>oI8W z`twC8a0lTQED3L%c$9)NBHry(G0h&7^#*CN%H^2TS~ym{!>N3XdvWDT80lMFS+Ej!B$KZ}d)Xb^j928T=aO~Dnvv5`T?0M5m|N0{c3+)tp#lA@NN>A{rlCfHqTno zvoTD6%$IMLi>r_G;lm^6%G%ev-sQk4_c2ZyAnQr}{#+(M=YEB+$<=&Pt3&J!&ULZ_ zbMg+`;l$o8a;CGa?t-t?TxT2)oQq^`<9!b7fW(nTW=QXQz0Vc{%314-EWv@XC|q|( zVf)u=iC6Yz=%}Xp4??>)I^!j%dVN#AQxI-0JRJPeiaU7+Yma+2aMv8-g?}yG#b&S* zb3@_P^5p#>-A>yNr_djFUNJCEOnwiL&MiXmM> zYkPo?hIedOmvNsW{Way)yyE*jPL_V^4Wt1%&nBihqRH!#t1dmwPIUqNo#5ddu)tHl zFH|$zy7vLPRKG93rnG)vfy%F%GuZ!3-#J$upN=*N z+PbQ|#{zbKf;MGF$mLQdT(g6ilyBOB@a zMN8fpXPhyxxuOuV%*vwmXV4$943z#P$VN?3-38awfKDl~gG!+up&aAashb=6X6@T{ zqH3co&dL>n7}eLGp`Ar|*0jpK^^;bf08Qy^d5iZGdWdlGMu6qE-(`-U#IuMcZl5G~ zP;&r1>^vtSEsEK0k<00 z6=z5lzg(i|84i9|T2C}xRNcGI4z;##wW~Qd&FKEqn+rEf#w{_;r@@Hh{O|;wCirI{g$TPOvLq3 z-SU!8_#dioxOTd!OYR?*tluKSwn>qgJ;)2$Z>IFS6_MdVFip@Hv|KB4u7ZsP|Ll)g zY^mG`)&c%Y9%alWsY!m}Wt?!EZMW1G{kkRCp;j}Q3!En*R^R(Q7RQ>WgwmJ^BN`t% zRoDvGR_#X3Z_WzIJCIO5{k}Q%E)h0z-^WQsi3zRfJk4v4dDb>iKl|u=aEv_a((v!&jwhsx6^VEVQ*H@0u%bf^r$jL-=CoEJiOOD(}P7K*Df2uX1kA-Iya@?q8*V=tJJ)SAoqS&!=EW^CZ73I3`}e z`r^S&82~g+k$xZ_;2#)&P2eFn{&s%{5ST9?=!~iWegv3Eo?D$~3Q{{f2Q4aOzmzzL24x}(86tTWt$9-b*GJG2u z_Ka`|(M}}9Aks>}9uVJP#TNlq@*Vz&9&f1>=VhMw0S9YWB-p}o0d0h05lgPJX)XZ; z>h35ohFp8fkmF`}W5`u5Z7%GKEBLDZ#nGWx^)IRUjsRM!aSP`x$7Fg{L?7lC!IZ@C z0v}O_%wlSL&(f8VZtRRN;u~tl#j2BI62s1@yYOpP{8}OR`j!wNg6X{Q)AHAIwJL=& zIe{Djo`d82?|XxBytMr~Nbg`u`+m38p9w%)yy38sFJDajr0_RNe>*CUduK~5@ogKy>$M{PC7snP zqCLpour1zWr1?^1A_gLM+w5zbM^4K7%Lxid!|gf*6|4sZjeL-=fHhn%aw#Q$3{3SO z;J0WgOpf6jWGhpdk%eY!+)#cIZY9#_LQQ+$`vnx4LC+@6OzjSYy2Q0YUwabc3c8*~ za3cah5HD~&-bg~}I$yG$U%#i&y6f*?%63PW*on)q=3hhV)LLz)Hp)V95Ff%GdUgfszSL~Vf8A>MEwJj=jcs%s`L^(f0=&AR zw|i@;G%1Tes&~V5CDCu{uf&g3c&8m&+y0iOW4QD<`lLK}p^|KMUj2rADq64_^<*tO z9Al&Cd&jUcxC|=Z&dDn~^oDmRyinO7n!k2nc$cbocGDN_{Wf(c&fSM~h)TAt$PVvM zUr_@=Ms?X_tlAHnxye@US)* z|2_==-1PdCUH&W)F32k}m+mRz0pv7V@lUi@7Y*>uyRQLD*x8(6->o@Oh!MYC9a>uKoygl9mN^Be?V@vSB-sCH}smCF+{aX{z?nNqo+LeP$(~Xzh4jd90)$a zqI-CO>~lTK?-bs^tj{cT5VCxdAsc~1W$?g zs_LY9+oYJ(`A3SiZ4Kg4W$I^)LPq`s(Wyu4cn5m;N{R$}m{?)|s_7>>l*)V^`Z@6X z^CgqAF69qNTb;S%^-e2=x&J>Fua})6$f!#QKb1RPZ*RH_7HTD4FPU=0>;2#a&Cxkt z?|_`~dRO_`yNcJFRVo#)H}03}GG6b$p-|m2Vbh`Pc)h<9io=|Pc2e%>m4IGo)Ug`D;N9m zdNX=ZTLESld(RxFeeNP&?+Jdrnelpe0(ClG@0TdIcvCuF?<;(5Gx2)g(ofdH5*cBF z2n*joA%#=%dYk+lnelq>mmH~hy+8MJWX9{AE;%yd^~$Ug13?VHsPmcZbHrY@-D`^N zp|FlKhd8}kLZq2b6IvOCP2CnEgh@&53v6I2_G-yV*ivMA*qM;$eS@6t1Z00@u}PANLxzhv zSxJ81e!#E2BA9B&+%oaA4cuz4*(0#6kx5&_B%$=gFgBs=vF<`Mqjk5X`-OvK`3}Y^ zHt;=r&3-9`lMa_UrQOeqqDx7mqv){3X^ME0)< zDhb9er5hndDPcRK==10jXH-d5`MWj4r6vwwEBr7*%`p@UJKdzDcNJyWeW&?Z6DWz+ zx0Fg#RpnpS{#_oT<*(NaBHlUWjwLKFI-P=G@Z?m%$`VuX87&wqZ>xPt)XE9ko;rou zOj7FQA8QoLN2YUSRq=Of1l!*4jux<`(-T@TJ7WccjC{6E@|95GSp$*s9}D(CWT{Y` z!Q#is$M;jwa^DNm(=KnZ?!Hr!zWrJ$*~3qzG6x~ctUrBh$05%N%dLjR&)SJpJW81yOx}t<_U8x{CLX2LaH3GGds1xZl(HE&;O8dF-Wyg!n-I0Ea>2n) zZIf3}3JDLLRHhRJ(x5Txu2GW7gryuR=B2YqSJrTsZucIMq39y8WJR8J*H0Kq3FjCs z-)%Ki>cj#cE7D;FXp@$NK4yGXqf1&1M+Vh5Yp|T{-WaNH`$fthB=NWr6jpqJ^54$r zP>GRyt5&O>@Yd1+snT|Bn3%M6(emeR!*9Mx?7@8k^6oDco5mjAoV=i2gg^suN-rgT z>|vNuz(xGYNdGeH&hu$2Xt-gs;KDtt4f9II&mCE!%b+SD*dYLN*4geD#`5Fa-{NcO z;P$q+)AK>U8a6`RNnZR{8Rct^_R9kB^!NO-?;CkoArqC&wU_hBx@$B5YkxEj?so61 zrawu%YjmWJit|i4ng!mc$R^=tt%f7Cfc95lHvQd|*uo~(} zN?w*uC$Oi24KCJuKSFe7;KFK{s2I-1$6A5iVBX>tkl8zfKazMGi6XkU9Zup=nz+vU7%WN7 z9?~*0=uQ89uh!7wh5edVNTGN5BQ>4oC;nOzU)02P-f#VqPwU5Z-jDqEKk;tFb7S4` z6-S;#AbE=kUTaS#er}$w&rMj_El;MB?~Wh*FRY;^jFcf{IlY%;;i-H3*g@FIiK0LJ zfNWM|VpDke4Unms1y&N8tLPWp58ZaMI?#96)|#T9yeoMXt`d=BLHkm1dcJ1%%Yhx4_Ol((E=Suq-K{%ML10GQlkhT^hOOrk z7Zna2bF8)S1QDYJ<4c?|k{WSNvJ+$3Y%`uReXV*~SBO~4PCJEPCgdYaBX0Gv+%)%m zal+1za39LTOSJq*DIak@&@DTa@sFlBC04_ONQz2p*+=Y;4%dGaYP-#sQ`WMx665lx zta+h0QvOyXF{;p7xL2#+U>A%jamEwDnoY3Hl3|K77{3Pai~*irF>B^L3Q=G#?RzrWZ*P>-l!8|E>A7=S zy~Bi!+^Xd!feQ<|2TNX6tr4gCSlby>;;SIqjk&|G$ZF@IjCP{#g%pqAsLs*OBQTj} zz?T@b;R+m;+6dwaI&!q5Z6

    rT=-~4Dd2LJD3=zhfpdVR&W`yx9Zq+&5Vx5uz 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

    !PobOg#D5}{cxL$GbSBwllb?Nu=-K) zU8i@P_5D{sezD2B?U5qWiW|c`#(eUiai8 zx!3Z>)q!*qL6&0p*I6Y}aBk0@kZu5N5_{2W2 z7MQ_9wj4B?l>-g^pUlUlpW=4a$qja9=)O|BX?>>MNk}Z`fj+syU%ayu*~_a6*0Rd< zs}gKKMP{Z>QG=KXlPBs9p}(#SX3gs=t>r4RaH`77d)TZ>tA|{!s;;bBu43G@K5~Pn zmxtV2CUykAY?G^vQBL;qQcm``Zu9hzdw5i+J8r}JPu^%+1wE2Dd1dezud1x9Sk7~h zSGk%VgJM)Ql{FAls#z)21mAHKk?9GYRIQXP(6DmNM%*wQ*^vc`Cq-x$+^djjwB)L4 zD{G-U&ze=ayJ#SmSA>Tu8`r6-QY+O;m0GP*tNe%8sj3bXL1nJ0qq05oS*1k;EFIyc zKbeZi2t`Q&2@$IDhzJR(sUl8|IK9fr@@C>3fvohLEUzvD^GY6*<#O4~?5J`tE)D(l zt>*1L*6PlhdlvZT&wjO1&vGp$zVmv|Yf;r+dw+Yi!lONYOsl)C$(5PCKRR2n!Vmur zUefmYpBJszQZ)a8?A+f-2v_x`cdZ&=u` z`5Qbu>n`xxxhXnkE#QaLjR$MYGQHaB-Bl5% ze&2og7 z?C{6sII6EtYe9J$LS||wxQZmz&M>I6p0YUsW?hTsK- zdYaOwoTvtR!I)M2Dxq-0tCY*af-R$0UpTyOeqg&f+Zt7h*#CNmiTj7`-lYGleBMI! zo-aP?`mr{6RD@;C^%t9%eth@aeyI(ckLjp8b?CJZe_b}qWSG37_T~CMb!K*4|JmPF za)w4$X*1FHSJ}7*+p{WZbKAiwU5dx1Sq^q;{=%Z*V_eS+t2 z>-tNcwbScM<8I}Aeydugm>z5HN1bc@-m9VCL1Fx<#9ohV32KtHG;lW`17l z%8XM(XaD)$;Da5!+iBjfe7@fXkLrG#W*pHEni+AdwYmJSlYbm|+XU^vKILT3wY(jW zdx-PM`@d@dlmSXVwZB^Br}Pbg2Ef-J8h~Je|4AeCsG9!6f(;d$Ecm8zZ|ybj8!zR) z+VNVznj!rMU7uZV=EZycQg4^v*i7khrEZVA+KOi;hOS?7@6_?6@e5cEz0&n>r{mOgnqZm2-B5pRB&3`_1-|ZR#KW=kpzV zs-1tS;a4ZR@947mR`qJKg_XB7t=-A;Ww)x|#Mj=qcFe-wF@rvSzjNreyMJ9aefHDZ z{+bC*`j6JW5jyjrCBCxHl+fwEvnFj(4!u6yOFbuL+x$is4d#E7Uoz~EJeR%7x0Ye` z*hR-W@|r$bJ`UQxY)Mrs=UTZPje*;${m8mMS&kh|BZb#wc?Ad&x+=MhdZvU#Z@F^P zAIf&+Dzq;r_x3LDA+OySTLv>U4P(cS6*Ud4kx0;YR!%x#ckH2@w6p%CZPA8QBYp@_ zWTY!BDMqsb?w+Dv%bc6TlLUSYsM8e~2GAM8ml*|q zT1eDqCMw`lQyO?kPgDQ}XCm7=YgS|=Dc~mQ4o(4?GQcke1}^R4!)E}632&V+Li9kU zg)PaOJ4vc0YraMVyxG5F&aNACmR-0xcG(}Ke&^QoxR>+q=%eMYj}M+QJ4SIP=-`%*mPS zmshFe80e|GE4xBP@bawU=QQ-e5fqVVc2R_;!vTJyLaWEi#1w5DzS{}|w#rU2X&_dq zN>wMHPD4GYa1S5)eEw0zLuog^$baxt*D>3z`SIOSKUkZonvpwx=EfU^HCL``x_j7x zRarT+i_c^y_ZkzgES~Y%^Rb!r_nl9v`twa&M*XopE*<#Xw_w_h5kI8LZ~Zz{cDiYg zrTu)?$Y#~q8JM}{+Uq+!uTDQ3G^hBJhP!7TI2^isob}EXMX0*Q=&jLvw=I5e)3qT* z39F`exjSdzx|U&M4=f*%74V?N*00;^Kc6@JNP`*)$Ah;I=s0Nk$h5iJ5B}Ug>A?2O zZL65#7B4=WeK_yMYKo~V?lrr=|HbC19xeCo-tzi{u-$|GKU^95)zs&co8s%gpm`$R#458AuW1=D3m*Og1_zYvUVfiKD?P%1VL^PfyV>^|Cd* zWgpq*?ISi;z5DjPEqk`@SU$wLXDe@oS7{$a%A0!Cs8StfDy%vynHsLFDRNL1gDYX6 z8Ps&6ZBVq&jzLFiExPMF{Y>cbW^;N>d0u}pcfq{_HO{@YE3024dCP69SATqD{DIXs zQ`b+d^wm$l>{F?R-e2;^>z>-|!H4s2_q(x3`_79!`qho8_|JxN{{}a_wSDW_uiyU1 z=es3c#y8$_Yu=0_>f!xcA3V9ZTeE|Wi_ed|aBEiiqf?iE_{yHk3j%h$_xfIIXuT^p zI|fEK4tf8^LFM1O-~2Z6t-@by)_LzLtBx)I_R;CFQC>y18YfeJpWa75@Yi$A|K3&L zHLKSA0k`Ao#ox{UUH644tzYDap7&oKSUa-z%Z*-mf8@B^dor(wFa98A{3lfp&wr)+ ztFK&sbJ_Pt!mspQ{b|?3y?;KvX2s0?J5`fwjZ{vqH4Nsb%1KlIhb^*41#vGSnkCyN z%fEn@SeXA-^i);0gYt4^Lp!MGt*Q>qIE+wXjOy#_@8>^U+1X+1rE2D-SUfUrM(0^W zb_Jh&W!RW0Lkq9O4*TF0g?3co-t$9!CjBr=*^N9TBv$lLbyjv1P1sA}d$PQ(!@pcL zSzaF+g4*4B=A@-6Xe&YYW~xrgW(v5oDg$?zc5gN)wpYiXW<7gU>ee(ODm-?WR-=GU z#B5P$2Zi;I3|BPk*ts+ID~8UUBVr>I+WwI-u?mpUxwB?qGetAEbq^exJHvWC%*ZXd zcvzq#c<7X9No)qeaOqCb0nhVX#Zn&`u zf268Z$;R;Z{RRgn_>c3=8J;j~;4o!W%Ag_f%I@f{Gdb+mnf!tY3!z(Q7JiRP;=usS z;31=Ap$D%Wx;pGycQ#AVnUrzHY z|6==7{XePJePxfgE3P#tRM9v3dXLj}xG{8Qk@~0BUgK|188l|jx0Wkc8(YsMST;@V zQGL&lA)hxsSM<)uH@>T5DpxOV?cruOg6rM?Y{tHpXOfpMs9*W@ZFj!dc%l3GsQs;X zJ=b8P`oOq3^)~mpoU9tC-LQ06;4Wp&qpK(T6e=hCd|IwDE%sFO@u;Sn>;qFlpI!En z3=2w=eb$4~IuAKi23QF4ZePvQqdp9%#)*8&y=rk}MODj7 z5m2pmz0Hd}cfPweJov5t6MozJ%BAQ2$e7cA*K;2?`?u}uXFMuc3MTj4yyl|~b+L(c zs#`FdmwQwQAGD!@_jPk4+vKXJVA$sgv({@g?0ZrE*;}jbRnHN>X&m;wqEfO|1Q&0@P@efK z>*ArwjRKQ5-EZ{Ht%>oMgC2a?>?L(}iy=W>{KG4q+dp>xKNogQXz3U9;g?6UK2~oV zc0Fm{?+gB3x?^0;Cbe@8Ec&rU$Pr(kfz8?+>pMBauj^ZfPRx2|--a=h%-Iz;kF+*l z`s3$mA1H6W@Wn@U6_+Y!HGN^;s}oM0SWpK_`77^!aN)9IqG#*;b*|>wEC4weK8V+-}+SA)lu2KRxnF@kh3d+wD&e zeeLUh$Cd^id@&&6m$t8~{%#)pX--u6g~zIn+|+Kwq>qGY>&GxY`rsj>G{4VU4*cw5 z;MAbiLtEXw_kG)oe>8jzys#xD9AkL{fPU*Ad!vhU&|+m9VPFA+lZI{^NSq!T67>I< z4946Kswyabaj@0ItAnzgvW=~kt;IB>K0LecJ!R#Hjq%Uj z=(_g6%rx(j`-i?3wQEpaZ{zFb{IW^nbJ_om%!V%)EbZt>vF=dYBXv-jW* z^SFpAZ~jrg$-WI;I}Q2ZtK9IHJ&zvD^nLr_{{3OyYu-QncEhCEaZ4)f+xg%V?VUx9 zih3SSDwsc{{k0Y!EZ%c}b8_7rUDu&|gW9ETsa!r{#_bay{;VB+XFAM$%fs~JH);mQ z_aBn5>SpmSX*2lWE#mtr{c$PZ&yO$SJK+DgDSp&s_RQG2tM9z&lRV;drI zAnf~!<15YDUH5@Gr1J*{&uI^}KKjpYKa;I$&2Ar57`XJcjNrhpR~l|_Pq>wLG%{_Z z;!N=075Coia`W@P#zgNodlh}{f6!33@!{HQpVM#ebh~(F)j1cs|5@&2#0>*U~VgHzwST72-md%q~=)jiV2@_mH?Gh2n_zVv6@WzEplf4va- z(!#z2_WbsRdB@jYFJF#cbz*J3k()*@NnQE`?6V#nX8HTvdPJzhJS zHZE58NBBIS!t4>tWK|jsJl^fI`#;_Jq(|ni-50O;z5BfFy%EXlt87}<*s$$#b!C{} z)%VWG&%E<(m^tf}7s975jq|ziUhC7NlaKjoGL9{2Y+E{UK#kWf8UMZBPjhqN@n+SA zoNZSA$7b87UtNAib0WRxkMnjv==J4@WBD7syQBrSS-ZZMA!E?$lo`K7_c(Uzi=XcI zT=zpO>y9p?zdG>aJ@urc>NR~X%`3OI-VfVObnW+D(Wlk6zQ5$ul(Kbv{xR`UHN9*d zAHUd@t>c%iV;HK4c*;MJ9IDXaS+YMtEd_(;k$qm9N$}9_4v-0 zUz(+u=KX1#<6E}JFK*mhJjA0ljfxRn&K%WG&pQA8*@`o}Z0)r*K~;Ri-}BuzLuU5M z4{Ke{zd_G8`d+;8{MNbKn>C-fF}m@%Rb3LNtStW6qo7>so=%JYiT-tUT)*BK-3GKD zqx$o{rtSXmvR1Qaj0{;h)!=#VyLAIvCe>}1S1rSHp7qG~@{#Q)y?%4$_>E1ce7UB3 z{Kb1G&lR-YcWe2EZBgIcxBfA--^KgqyPf#$*!eS)<}ItSxZL5eUqAdV@|RPmXRO&( zH0s2n9@)RXKmTNJS^R~~qn=YOZ*(bp)Y!HyryO3ZeIs(>&mRuW(dPUe^~v<9<|B6& zZK}L6`%bN|&V;1o?D}nk$C>q;_J#e~w!*mg1AE_jf5pD6ALmEPW>uPC-$PUOqbvu? za-b{+%5tDA2g-7wEC new CallTree.Root(tracer)), + rootContext.serialize(), + rootContext.getServiceName(), + rootContext.getServiceVersion(), + 0); + NoopObjectPool callTreePool = NoopObjectPool.ofRecyclable(CallTree::new); + root.addStackTrace(tracer, List.of(StackFrame.of("A", "a")), 0, callTreePool, 0); + + TraceContext spanContext = TraceContext.with64BitId(tracer); + TraceContext.fromParentContext().asChildOf(spanContext, rootContext); + + root.onActivation(spanContext.serialize(), TimeUnit.MILLISECONDS.toNanos(5)); + root.addStackTrace( + tracer, + List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(10), + callTreePool, + 0); + root.addStackTrace( + tracer, + List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(20), + callTreePool, + 0); + root.onDeactivation( + spanContext.serialize(), rootContext.serialize(), TimeUnit.MILLISECONDS.toNanos(25)); + + root.addStackTrace( + tracer, + List.of(StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(30), + callTreePool, + 0); + root.end(callTreePool, 0); + + System.out.println(root); + + assertThat(root.getCount()).isEqualTo(4); + assertThat(root.getDurationUs()).isEqualTo(30_000); + assertThat(root.getChildren()).hasSize(1); + + CallTree a = root.getLastChild(); + assertThat(a).isNotNull(); + assertThat(a.getFrame().getMethodName()).isEqualTo("a"); + assertThat(a.getCount()).isEqualTo(4); + assertThat(a.getDurationUs()).isEqualTo(30_000); + assertThat(a.getChildren()).hasSize(1); + + CallTree b = a.getLastChild(); + assertThat(b).isNotNull(); + assertThat(b.getFrame().getMethodName()).isEqualTo("b"); + assertThat(b.getCount()).isEqualTo(2); + assertThat(b.getDurationUs()).isEqualTo(10_000); + assertThat(b.getChildren()).isEmpty(); + + root.spanify(); + + assertThat(reporter.getSpans()).hasSize(2); + assertThat(reporter.getSpans().get(1).getTraceContext().isChildOf(spanContext)); + assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(rootContext)); + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java new file mode 100644 index 000000000..d3a11e994 --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java @@ -0,0 +1,1043 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import static java.util.stream.Collectors.toMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; + +import co.elastic.apm.agent.MockReporter; +import co.elastic.apm.agent.MockTracer; +import co.elastic.apm.agent.configuration.SpyConfiguration; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.sampling.ConstantSampler; +import co.elastic.apm.agent.impl.transaction.AbstractSpan; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.StackFrame; +import co.elastic.apm.agent.impl.transaction.TraceContext; +import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.objectpool.NoopObjectPool; +import co.elastic.apm.agent.objectpool.ObjectPool; +import co.elastic.apm.agent.objectpool.impl.ListBasedObjectPool; +import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; +import co.elastic.apm.agent.tracer.configuration.TimeDuration; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.stagemonitor.configuration.ConfigurationRegistry; + +@DisabledOnOs(OS.WINDOWS) +@DisabledOnAppleSilicon +class CallTreeTest { + + private MockReporter reporter; + private ElasticApmTracer tracer; + private ProfilingConfiguration profilerConfig; + + @BeforeEach + void setUp() { + reporter = new MockReporter(); + ConfigurationRegistry config = SpyConfiguration.createSpyConfig(); + // disable scheduled profiling to not interfere with this test + profilerConfig = config.getConfig(ProfilingConfiguration.class); + doReturn(true).when(profilerConfig).isProfilingEnabled(); + tracer = MockTracer.createRealTracer(reporter, config, false); + } + + @AfterEach + void tearDown() throws IOException { + Objects.requireNonNull(tracer.getLifecycleListener(ProfilingFactory.class)) + .getProfiler() + .clear(); + tracer.stop(); + } + + @Test + void testCallTree() { + TraceContext traceContext = TraceContext.with64BitId(MockTracer.create()); + CallTree.Root root = + CallTree.createRoot( + NoopObjectPool.ofRecyclable(() -> new CallTree.Root(tracer)), + traceContext.serialize(), + traceContext.getServiceName(), + traceContext.getServiceVersion(), + 0); + ObjectPool callTreePool = + ListBasedObjectPool.ofRecyclable(new ArrayList<>(), Integer.MAX_VALUE, CallTree::new); + root.addStackTrace(tracer, List.of(StackFrame.of("A", "a")), 0, callTreePool, 0); + root.addStackTrace( + tracer, + List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(10), + callTreePool, + 0); + root.addStackTrace( + tracer, + List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(20), + callTreePool, + 0); + root.addStackTrace( + tracer, + List.of(StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(30), + callTreePool, + 0); + root.end(callTreePool, 0); + + System.out.println(root); + + assertThat(root.getCount()).isEqualTo(4); + assertThat(root.getDepth()).isEqualTo(0); + assertThat(root.getChildren()).hasSize(1); + + CallTree a = root.getLastChild(); + assertThat(a).isNotNull(); + assertThat(a.getFrame().getMethodName()).isEqualTo("a"); + assertThat(a.getCount()).isEqualTo(4); + assertThat(a.getChildren()).hasSize(1); + assertThat(a.getDepth()).isEqualTo(1); + assertThat(a.isSuccessor(root)).isTrue(); + + CallTree b = a.getLastChild(); + assertThat(b).isNotNull(); + assertThat(b.getFrame().getMethodName()).isEqualTo("b"); + assertThat(b.getCount()).isEqualTo(2); + assertThat(b.getChildren()).isEmpty(); + assertThat(b.getDepth()).isEqualTo(2); + assertThat(b.isSuccessor(a)).isTrue(); + assertThat(b.isSuccessor(root)).isTrue(); + } + + @Test + void testGiveEmptyChildIdsTo() { + CallTree rich = new CallTree(); + rich.addChildId(42); + CallTree robinHood = new CallTree(); + CallTree poor = new CallTree(); + + rich.giveLastChildIdTo(robinHood); + robinHood.giveLastChildIdTo(poor); + // list is not null but empty, expecting no exception + robinHood.giveLastChildIdTo(rich); + + assertThat(rich.hasChildIds()).isFalse(); + assertThat(robinHood.hasChildIds()).isFalse(); + assertThat(poor.hasChildIds()).isTrue(); + } + + @Test + void testTwoDistinctInvocationsOfMethodBShouldNotBeFoldedIntoOne() throws Exception { + assertCallTree( + new String[] {" bb bb", "aaaaaa"}, + new Object[][] { + {"a", 6}, + {" b", 2}, + {" b", 2} + }); + } + + @Test + void testBasicCallTree() throws Exception { + assertCallTree( + new String[] {" cc ", " bbb", "aaaa"}, + new Object[][] { + {"a", 4}, + {" b", 3}, + {" c", 2} + }, + new Object[][] { + {"a", 3}, + {" b", 2}, + {" c", 1} + }); + } + + @Test + void testShouldNotCreateInferredSpansForPillarsAndLeafShouldHaveStacktrace() throws Exception { + assertCallTree( + new String[] {" dd ", " cc ", " bb ", "aaaa"}, + new Object[][] { + {"a", 4}, + {" b", 2}, + {" c", 2}, + {" d", 2} + }, + new Object[][] { + {"a", 3}, + {" d", 1, List.of("c", "b")} + }); + } + + @Test + void testRemoveNodesWithCountOne() throws Exception { + assertCallTree( + new String[] {" b ", "aaa"}, new Object[][] {{"a", 3}}, new Object[][] {{"a", 2}}); + } + + @Test + void testSameTopOfStackDifferentBottom() throws Exception { + assertCallTree( + new String[] {"cccc", "aabb"}, + new Object[][] { + {"a", 2}, + {" c", 2}, + {"b", 2}, + {" c", 2}, + }); + } + + @Test + void testStackTraceWithRecursion() throws Exception { + assertCallTree( + new String[] {"bbccbbcc", "bbbbbbbb", "aaaaaaaa"}, + new Object[][] { + {"a", 8}, + {" b", 8}, + {" b", 2}, + {" c", 2}, + {" b", 2}, + {" c", 2}, + }); + } + + @Test + void testFirstInferredSpanShouldHaveNoStackTrace() throws Exception { + assertCallTree( + new String[] {"bb", "aa"}, + new Object[][] { + {"a", 2}, + {" b", 2}, + }, + new Object[][] { + {"b", 1}, + }); + } + + @Test + void testCallTreeWithSpanActivations() throws Exception { + assertCallTree( + new String[] {" cc ee ", " bbb dd ", " a aaaaaa a ", "1 2 2 1"}, + new Object[][] { + {"a", 8}, + {" b", 3}, + {" c", 2}, + {" d", 2}, + {" e", 2}, + }, + new Object[][] { + {"1", 11}, + {" a", 9}, + {" 2", 7}, + {" b", 2}, + {" c", 1}, + {" e", 1, List.of("d")}, + }); + } + + /* + * [1 ] [1 ] + * [a ] [a ] + * [2 ] ─┐ [b ] + * [b ] │ [c ] + * [c ] └► [2 ] + * [] [] + */ + @Test + void testDeactivationBeforeEnd() throws Exception { + assertCallTree( + new String[] { + " dd ", + " cccc c ", + " bbbb bb ", // <- deactivation for span 2 happens before b and c ends + " a aaaa aa ", // that means b and c must have started before 2 has been activated + "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 + }, + new Object[][] { + {"a", 7}, + {" b", 6}, + {" c", 5}, + {" d", 2}, + }, + new Object[][] { + {"1", 10}, + {" a", 8}, + {" b", 7}, + {" c", 6}, + {" 2", 5}, + {" d", 1}, + }); + } + + /* + * [1 ] [1 ] + * [a ] [a ] + * [2 ] [3] [b ][3] <- b is supposed to stealChildIdsFom(a) + * [b ] [2 ] however, it should only steal 2, not 3 + */ + @Test + void testDectivationBeforeEnd2() throws Exception { + assertCallTree( + new String[] {" bbbb b ", " a aaaa a a a ", "1 2 2 3 3 1"}, + new Object[][] { + {"a", 8}, + {" b", 5}, + }, + new Object[][] { + {"1", 13}, + {" a", 11}, + {" b", 6}, + {" 2", 5}, + {" 3", 2}, + }); + } + + /* + * [a ] [a ] + * [1] [1] + * [2] [c ] + * [b] [b ] <- b should steal 2 but not 1 from a + * [c] [2] + */ + @Test + void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations() throws Exception { + Map> spans = + assertCallTree( + new String[] {" c c ", " b b ", "a a a aa", " 1 1 2 2 "}, + new Object[][] { + {"a", 5}, + {" b", 2}, + {" c", 2}, + }, + new Object[][] { + {"a", 9}, + {" 1", 2}, + {" c", 3, List.of("b")}, + {" 2", 2}, + }); + assertThat(spans.get("a").getChildIds().getSize()).isEqualTo(1); + assertThat(spans.get("c").getChildIds().getSize()).isEqualTo(1); + } + + /* + * [a ] [a ] + * [1] [1] + * [2 ] [c ] <- this is an open issue: c should start when 2 starts but starts with 3 starts + * [3] [2 ] + * [c ] [3] + */ + @Test + void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations_Nested() throws Exception { + Map> spans = + assertCallTree( + new String[] {" c c ", " b b ", "a a a aa", " 1 1 23 32 "}, + new Object[][] { + {"a", 5}, + {" b", 2}, + {" c", 2}, + }, + new Object[][] { + {"a", 11}, + {" 1", 2}, + {" c", 4, List.of("b")}, + {" 2", 4}, + {" 3", 2}, + }); + assertThat(spans.get("a").getChildIds().getSize()).isEqualTo(1); + assertThat(spans.get("c").getChildIds().getSize()).isEqualTo(1); + } + + /* + * [a ] [a ] + * [b[1] - > [b[1] + */ + @Test + void testActivationAfterMethodEnds() throws Exception { + assertCallTree( + new String[] {"bb ", "aa a ", " 1 1"}, + new Object[][] { + {"a", 3}, + {" b", 2}, + }, + new Object[][] { + {"a", 3}, + {" b", 1}, + {" 1", 2} + }); + } + + /* + * [a ] + * [b[1] + */ + @Test + void testActivationBetweenMethods() throws Exception { + assertCallTree( + new String[] {"bb ", "aa a", " 11 "}, + new Object[][] { + {"a", 3}, + {" b", 2}, + }, + new Object[][] { + {"a", 4}, + {" b", 1}, + {" 1", 1}, + }); + } + + /* + * [a ] + * [b[1] + * c + */ + @Test + void testActivationBetweenMethods_AfterFastMethod() throws Exception { + assertCallTree( + new String[] {" c ", "bb ", "aa a", " 11 "}, + new Object[][] { + {"a", 3}, + {" b", 2}, + }, + new Object[][] { + {"a", 4}, + {" b", 1}, + {" 1", 1}, + }); + } + + /* + * [a ] + * [b] + * 1 + */ + @Test + void testActivationBetweenFastMethods() throws Exception { + assertCallTree( + new String[] {"c d ", "b b ", "a a a", " 11 22 "}, + new Object[][] { + {"a", 3}, + {" b", 2}, + }, + new Object[][] { + {"a", 6}, + {" b", 3}, + {" 1", 1}, + {" 2", 1}, + }); + } + + /* */ + /* + * [a ] + * [b] [1 [c] + */ + /* + @Test + void testActivationBetweenMethods_WithCommonAncestor() throws Exception { + assertCallTree(new String[]{ + " c f g ", + "bbb e d dd", + "aaa a a aa", + " 11 22 33 " + }, new Object[][] { + {"a", 7}, + {" b", 3}, + {" d", 3}, + }, new Object[][] { + {"a", 12}, + {" b", 2}, + {" 1", 1}, + {" 2", 1}, + {" d", 4}, + {" 3", 1}, + }); + }*/ + + /* + * [a ] + * [1 ] + * [2] + */ + @Test + void testNestedActivation() throws Exception { + Map> spans = + assertCallTree( + new String[] {"a a a", " 12 21 "}, + new Object[][] { + {"a", 3}, + }, + new Object[][] { + {"a", 6}, + {" 1", 4}, + {" 2", 2}, + }); + } + + /* + * [1 ] + * [a][2 ] + * [b] [3 ] + * [c] + */ + @Test + void testNestedActivationAfterMethodEnds_RootChangesToC() throws Exception { + Map> spans = + assertCallTree( + new String[] {" bbb ", " aaa ccc ", "1 23 321"}, + new Object[][] { + {"a", 3}, + {" b", 3}, + {"c", 3}, + }, + new Object[][] { + {"1", 11}, + {" b", 2, List.of("a")}, + {" 2", 6}, + {" 3", 4}, + {" c", 2} + }); + + if (spans.get("b").getChildIds() != null) { + assertThat(spans.get("b").getChildIds().isEmpty()).isTrue(); + } + } + + /* + * [1 ] + * [a ][3 ] + * [b ] [4 ] + * [2] [c] + */ + @Test + void testRegularActivationFollowedByNestedActivationAfterMethodEnds() throws Exception { + assertCallTree( + new String[] {" d ", " b b b ", " a a a ccc ", "1 2 2 34 431"}, + new Object[][] { + {"a", 3}, + {" b", 3}, + {"c", 3}, + }, + new Object[][] { + {"1", 13}, + {" b", 4, List.of("a")}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} + }); + } + + /* + * [1 ] + * [a ] + * [b ][3 ] + * [2] [4 ] + * [c] + */ + @Test + void testNestedActivationAfterMethodEnds_CommonAncestorA() throws Exception { + Map> spans = + assertCallTree( + new String[] {" b b b ccc ", " aa a a aaa a ", "1 2 2 34 43 1"}, + new Object[][] { + {"a", 8}, + {" b", 3}, + {" c", 3}, + }, + new Object[][] { + {"1", 15}, + {" a", 13}, + {" b", 4}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} + }); + + assertThat(spans.get("b").getChildIds().toArray()) + .containsExactly(spans.get("2").getTraceContext().getId().readLong(0)); + assertThat(spans.get("c").getChildIds()).isNull(); + // only has 3 as a child as 4 is a nested activation + assertThat(spans.get("a").getChildIds().toArray()) + .containsExactly(spans.get("3").getTraceContext().getId().readLong(0)); + } + + /* + * [1 ] + * [a] + * [2 ] + * [b] + * [c] + */ + @Test + void testActivationAfterMethodEnds_RootChangesToB() throws Exception { + assertCallTree( + new String[] {" ccc ", " aaa bbb ", "1 2 21"}, + new Object[][] { + {"a", 3}, + {"b", 3}, + {" c", 3}, + }, + new Object[][] { + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" c", 2, List.of("b")} + }); + } + + /* + * [1 ] + * [a] + * [2 ] + * [b] + */ + @Test + void testActivationAfterMethodEnds_RootChangesToB2() throws Exception { + assertCallTree( + new String[] {" aaa bbb ", "1 2 21"}, + new Object[][] { + {"a", 3}, + {"b", 3}, + }, + new Object[][] { + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" b", 2} + }); + } + + /* + * [1] + * [a] + @Test + void testActivationBeforeCallTree() throws Exception { + assertCallTree(new String[]{ + " aaa", + "1 1 " + }, new Object[][] { + {"a", 3}, + }, new Object[][] { + {"a", 3}, + {" 1", 2}, + }); + } */ + + /* + * [1 ] + * [a ] + * [2 ] + * [b] + * [c] + */ + @Test + void testActivationAfterMethodEnds_SameRootDeeperStack() throws Exception { + assertCallTree( + new String[] {" ccc ", " aaa aaa ", "1 2 21"}, + new Object[][] { + {"a", 6}, + {" c", 3}, + }, + new Object[][] { + {"1", 9}, + {" a", 6}, + {" 2", 4}, + {" c", 2} + }); + } + + /* + * [1 ] + * [a ] + * [2 ] + * [b] + */ + @Test + void testActivationBeforeMethodStarts() throws Exception { + assertCallTree( + new String[] {" bbb ", " a aaa a ", "1 2 2 1"}, + new Object[][] { + {"a", 5}, + {" b", 3}, + }, + new Object[][] { + {"1", 8}, + {" a", 6}, + {" 2", 4}, + {" b", 2} + }); + } + + /* + * [1 ] [1 ] + * [a ] [a ] + * [b ] -> [b ] + * [c ] -> [c ] + * [2 ] [2 ] + * [] [] + */ + @Test + void testDectivationAfterEnd() throws Exception { + assertCallTree( + new String[] { + " dd ", + " c ccc ", + " bb bbb ", // <- deactivation for span 2 happens after b ends + " aaa aaa aa ", // that means b must have ended after 2 has been deactivated + "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 + }, + new Object[][] { + {"a", 8}, + {" b", 5}, + {" c", 4}, + {" d", 2}, + }, + new Object[][] { + {"1", 11}, + {" a", 9}, + {" b", 6}, + {" c", 5}, + {" 2", 4}, + {" d", 1}, + }); + } + + @Test + void testCallTreeActivationAsParentOfFastSpan() throws Exception { + assertCallTree( + new String[] {" b ", " aa a aa ", "1 2 2 1"}, + new Object[][] {{"a", 5}}, + new Object[][] { + {"1", 8}, + {" a", 6}, + {" 2", 2}, + }); + } + + @Test + void testCallTreeActivationAsChildOfFastSpan() throws Exception { + doReturn(TimeDuration.of("50ms")).when(profilerConfig).getInferredSpansMinDuration(); + assertCallTree( + new String[] {" c c ", " b b ", " aaa aaa ", "1 22 1"}, + new Object[][] {{"a", 6}}, + new Object[][] { + {"1", 9}, + {" a", 7}, + {" 2", 1}, + }); + } + + @Test + void testCallTreeActivationAsLeaf() throws Exception { + assertCallTree( + new String[] {" aa aa ", "1 22 1"}, + new Object[][] {{"a", 4}}, + new Object[][] { + {"1", 7}, + {" a", 5}, + {" 2", 1}, + }); + } + + @Test + void testCallTreeMultipleActivationsAsLeaf() throws Exception { + assertCallTree( + new String[] {" aa aaa aa ", "1 22 33 1"}, + new Object[][] {{"a", 7}}, + new Object[][] { + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, + }); + } + + @Test + void testCallTreeMultipleActivationsAsLeafWithExcludedParent() throws Exception { + doReturn(TimeDuration.of("50ms")).when(profilerConfig).getInferredSpansMinDuration(); + // min duration 4 + assertCallTree( + new String[] {" b b c c ", " aa aaa aa ", "1 22 33 1"}, + new Object[][] {{"a", 7}}, + new Object[][] { + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, + }); + } + + @Test + void testCallTreeMultipleActivationsWithOneChild() throws Exception { + assertCallTree( + new String[] {" bb ", " aa aaa aa aa ", "1 22 3 3 1"}, + new Object[][] { + {"a", 9}, + {" b", 2} + }, + new Object[][] { + {"1", 14}, + {" a", 12}, + {" 2", 1}, + {" 3", 3}, + {" b", 1}, + }); + } + + /* + * [1 ] [1 ] + * [2] -> [a ] + * [a] [2] + * + * Note: this test is currently failing + */ + @Test + @Disabled("fix me") + void testNestedActivationBeforeCallTree() throws Exception { + assertCallTree( + new String[] {" aaa ", "12 2 1"}, + new Object[][] { + {"a", 3}, + }, + new Object[][] { + {"1", 5}, + {" a", 3}, // a is actually a child of the transaction + {" 2", 2}, // 2 is not within the child_ids of a + }); + } + + private void assertCallTree(String[] stackTraces, Object[][] expectedTree) throws Exception { + assertCallTree(stackTraces, expectedTree, null); + } + + private Map> assertCallTree( + String[] stackTraces, Object[][] expectedTree, @Nullable Object[][] expectedSpans) + throws Exception { + CallTree.Root root = getCallTree(tracer, stackTraces); + StringBuilder expectedResult = new StringBuilder(); + for (int i = 0; i < expectedTree.length; i++) { + Object[] objects = expectedTree[i]; + expectedResult.append(objects[0]).append(" ").append(objects[1]); + if (i != expectedTree.length - 1) { + expectedResult.append("\n"); + } + } + + String actualResult = root.toString().replace(CallTreeTest.class.getName() + ".", ""); + actualResult = + Arrays.stream(actualResult.split("\n")) + // skip root node + .skip(1) + // trim first two spaces + .map(s -> s.substring(2)) + .collect(Collectors.joining("\n")); + + assertThat(actualResult).isEqualTo(expectedResult.toString()); + + if (expectedSpans != null) { + root.spanify(); + Map> spans = + reporter.getSpans().stream() + .collect(toMap(s -> s.getNameAsString().replaceAll(".*#", ""), Function.identity())); + assertThat(reporter.getSpans()).hasSize(expectedSpans.length); + spans.put(null, reporter.getTransactions().get(0)); + + for (int i = 0; i < expectedSpans.length; i++) { + Object[] expectedSpan = expectedSpans[i]; + String spanName = ((String) expectedSpan[0]).trim(); + long durationMs = (int) expectedSpan[1] * 10; + List stackTrace = + expectedSpan.length == 3 ? (List) expectedSpan[2] : List.of(); + int nestingLevel = getNestingLevel((String) expectedSpan[0]); + String parentName = getParentName(expectedSpans, i, nestingLevel); + assertThat(spans).containsKey(spanName); + assertThat(spans).containsKey(parentName); + AbstractSpan span = spans.get(spanName); + assertThat(span.isChildOf(spans.get(parentName))) + .withFailMessage( + "Expected %s (%s) to be a child of %s (%s) but was %s (%s)", + spanName, + span.getTraceContext().getId(), + parentName, + spans.get(parentName).getTraceContext().getId(), + reporter.getSpans().stream() + .filter( + s -> + s.getTraceContext() + .getId() + .equals(span.getTraceContext().getParentId())) + .findAny() + .map(Span::getNameAsString) + .orElse(null), + span.getTraceContext().getParentId()) + .isTrue(); + assertThat(spans.get(parentName).isChildOf(span)) + .withFailMessage( + "Expected %s (%s) to not be a child of %s (%s) but was %s (%s)", + parentName, + spans.get(parentName).getTraceContext().getId(), + spanName, + span.getTraceContext().getId(), + reporter.getSpans().stream() + .filter( + s -> + s.getTraceContext() + .getId() + .equals(span.getTraceContext().getParentId())) + .findAny() + .map(Span::getNameAsString) + .orElse(null), + span.getTraceContext().getParentId()) + .isFalse(); + assertThat(span.getDuration()) + .describedAs("Unexpected duration for span %s", span) + .isEqualTo(durationMs * 1000); + assertThat( + Objects.requireNonNullElse(((Span) span).getStackFrames(), List.of()) + .stream() + .map(StackFrame::getMethodName) + .collect(Collectors.toList())) + .isEqualTo(stackTrace); + } + return spans; + } + return null; + } + + @Nullable + private String getParentName(@Nonnull Object[][] expectedSpans, int i, int nestingLevel) { + if (nestingLevel > 0) { + for (int j = i - 1; j >= 0; j--) { + String name = (String) expectedSpans[j][0]; + boolean isParent = getNestingLevel(name) == nestingLevel - 1; + if (isParent) { + return name.trim(); + } + } + } + return null; + } + + private int getNestingLevel(String spanName) { + // nesting is denoted by two spaces + return ((spanName).length() - 1) / 2; + } + + public static CallTree.Root getCallTree(ElasticApmTracer tracer, String[] stackTraces) + throws Exception { + ProfilingFactory profilingFactory = tracer.getLifecycleListener(ProfilingFactory.class); + assertThat(profilingFactory).isNotNull(); + + SamplingProfiler profiler = profilingFactory.getProfiler(); + FixedNanoClock nanoClock = (FixedNanoClock) profilingFactory.getNanoClock(); + nanoClock.setNanoTime(0); + profiler.setProfilingSessionOngoing(true); + Transaction transaction = + tracer + .startRootTransaction(ConstantSampler.of(true), 0, null) + .withName("Call Tree Root") + .activate(); + transaction.getTraceContext().getClock().init(0, 0); + Map> spanMap = new HashMap<>(); + List stackTraceEvents = new ArrayList<>(); + for (int i = 0; i < stackTraces[0].length(); i++) { + nanoClock.setNanoTime(i * TimeUnit.MILLISECONDS.toNanos(10)); + List trace = new ArrayList<>(); + for (String stackTrace : stackTraces) { + char c = stackTrace.charAt(i); + if (Character.isDigit(c)) { + handleSpanEvent(tracer, spanMap, Character.toString(c), nanoClock.nanoTime()); + break; + } else if (!Character.isSpaceChar(c)) { + trace.add(StackFrame.of(CallTreeTest.class.getName(), Character.toString(c))); + } + } + if (!trace.isEmpty()) { + stackTraceEvents.add(new StackTraceEvent(trace, nanoClock.nanoTime())); + } + } + profiler.consumeActivationEventsFromRingBufferAndWriteToFile(); + long eof = profiler.startProcessingActivationEventsFile(); + CallTree.Root root = null; + NoopObjectPool callTreePool = NoopObjectPool.ofRecyclable(CallTree::new); + for (StackTraceEvent stackTraceEvent : stackTraceEvents) { + profiler.processActivationEventsUpTo(stackTraceEvent.nanoTime, eof); + if (root == null) { + root = profiler.getRoot(); + assertThat(root).isNotNull(); + } + long millis = + tracer.getConfig(ProfilingConfiguration.class).getInferredSpansMinDuration().getMillis(); + root.addStackTrace( + tracer, + stackTraceEvent.trace, + stackTraceEvent.nanoTime, + callTreePool, + TimeUnit.MILLISECONDS.toNanos(millis)); + } + transaction.deactivate().end(nanoClock.nanoTime() / 1000); + assertThat(root).isNotNull(); + root.end(callTreePool, 0); + return root; + } + + private static class StackTraceEvent { + + private final List trace; + private final long nanoTime; + + public StackTraceEvent(List trace, long nanoTime) { + + this.trace = trace; + this.nanoTime = nanoTime; + } + } + + private static void handleSpanEvent( + ElasticApmTracer tracer, Map> spanMap, String name, long nanoTime) { + if (!spanMap.containsKey(name)) { + Span span = tracer.getActive().createSpan(nanoTime / 1000).appendToName(name).activate(); + spanMap.put(name, span); + } else { + spanMap.get(name).deactivate().end(nanoTime / 1000); + } + } + + public static TraceContext rootTraceContext(ElasticApmTracer tracer) { + TraceContext traceContext = TraceContext.with64BitId(tracer); + traceContext.asRootSpan(ConstantSampler.of(true)); + return traceContext; + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java new file mode 100644 index 000000000..ad7b87026 --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import co.elastic.apm.agent.MockTracer; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.transaction.TraceContext; +import co.elastic.apm.agent.objectpool.ObjectPoolFactory; +import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +public class SamplingProfilerQueueTest { + + @Test + @DisabledOnOs(OS.WINDOWS) + @DisabledOnAppleSilicon + void testFillQueue() throws Exception { + System.out.println(System.getProperty("os.name")); + + ElasticApmTracer tracer = MockTracer.create(); + when(tracer.getObjectPoolFactory()).thenReturn(new ObjectPoolFactory()); + + SamplingProfiler profiler = new SamplingProfiler(tracer, new SystemNanoClock()); + + profiler.setProfilingSessionOngoing(true); + TraceContext traceContext = TraceContext.with64BitId(tracer); + + assertThat(profiler.onActivation(traceContext, null)).isTrue(); + long timeAfterFirstEvent = System.nanoTime(); + Thread.sleep(1); + + for (int i = 0; i < SamplingProfiler.RING_BUFFER_SIZE - 1; i++) { + assertThat(profiler.onActivation(traceContext, null)).isTrue(); + } + + // no more free slots after adding RING_BUFFER_SIZE events + assertThat(profiler.onActivation(traceContext, null)).isFalse(); + + profiler.consumeActivationEventsFromRingBufferAndWriteToFile(); + + // now there should be free slots + assertThat(profiler.onActivation(traceContext, null)).isTrue(); + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java new file mode 100644 index 000000000..bc496b587 --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import co.elastic.apm.agent.MockReporter; +import co.elastic.apm.agent.MockTracer; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Can be used in combination with the files created by {@link + * ProfilingConfiguration#backupDiagnosticFiles} to replay the creation of profiler-inferred spans. + * This is useful, for example, to troubleshoot why {@link + * co.elastic.apm.agent.impl.transaction.Span#childIds} are set as expected. + */ +public class SamplingProfilerReplay { + + private static final Logger logger = LoggerFactory.getLogger(SamplingProfilerReplay.class); + + public static void main(String[] args) throws Exception { + ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true); + File activationEventsFile = File.createTempFile("activations", ".dat"); + activationEventsFile.deleteOnExit(); + File jfrFile = File.createTempFile("traces", ".jfr"); + jfrFile.deleteOnExit(); + MockReporter reporter = new MockReporter(); + SamplingProfiler samplingProfiler = + new SamplingProfiler( + MockTracer.createRealTracer(reporter), + new SystemNanoClock(), + activationEventsFile, + jfrFile); + Path baseDir = Paths.get(System.getProperty("java.io.tmpdir"), "profiler"); + List activationFiles = + Files.list(baseDir) + .filter(p -> p.toString().endsWith("activations.dat")) + .sorted() + .collect(Collectors.toList()); + List traceFiles = + Files.list(baseDir) + .filter(p -> p.toString().endsWith("traces.jfr")) + .sorted() + .collect(Collectors.toList()); + if (traceFiles.size() != activationFiles.size()) { + throw new IllegalStateException(); + } + for (int i = 0; i < activationFiles.size(); i++) { + logger.info("processing {} {}", activationFiles.get(i), traceFiles.get(i)); + samplingProfiler.copyFromFiles(activationFiles.get(i), traceFiles.get(i)); + samplingProfiler.processTraces(); + } + logger.info("{}", reporter.getSpans()); + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java new file mode 100644 index 000000000..4dc80e38b --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java @@ -0,0 +1,324 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.doReturn; + +import co.elastic.apm.agent.MockReporter; +import co.elastic.apm.agent.MockTracer; +import co.elastic.apm.agent.common.util.WildcardMatcher; +import co.elastic.apm.agent.configuration.SpyConfiguration; +import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.transaction.Span; +import co.elastic.apm.agent.impl.transaction.Transaction; +import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; +import co.elastic.apm.agent.tracer.Scope; +import co.elastic.apm.agent.tracer.configuration.TimeDuration; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledForJreRange; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.condition.OS; +import org.stagemonitor.configuration.ConfigurationRegistry; + +// async-profiler doesn't work on Windows +@DisabledOnOs(OS.WINDOWS) +@DisabledOnAppleSilicon +class SamplingProfilerTest { + + private MockReporter reporter; + + @Nullable private ElasticApmTracer tracer; + private SamplingProfiler profiler; + private ProfilingConfiguration profilingConfig; + + @BeforeEach + void setup() { + // avoids any test failure to make other tests to fail + getProfilerTempFiles().forEach(SamplingProfilerTest::silentDeleteFile); + } + + @AfterEach + void tearDown() { + if (tracer != null) { + tracer.stop(); + } + + getProfilerTempFiles().forEach(SamplingProfilerTest::silentDeleteFile); + } + + @Test + void shouldLazilyCreateTempFilesAndCleanThem() throws Exception { + + List tempFiles = getProfilerTempFiles(); + assertThat(tempFiles).isEmpty(); + + // temporary files should be created on-demand, and properly deleted afterwards + setupProfiler(false); + + assertThat(profiler.getProfilingSessions()) + .describedAs("profiler should not have any session when disabled") + .isEqualTo(0); + + assertThat(getProfilerTempFiles()) + .describedAs("should not create a temp file when disabled") + .isEmpty(); + + doReturn(true).when(profilingConfig).isProfilingEnabled(); + + awaitProfilerStarted(profiler); + + assertThat(getProfilerTempFiles()).describedAs("should have created two temp files").hasSize(2); + + profiler.stop(); + + assertThat(getProfilerTempFiles()) + .describedAs("should delete temp files when profiler is stopped") + .isEmpty(); + } + + private static List getProfilerTempFiles() { + Path tempFolder = Paths.get(System.getProperty("java.io.tmpdir")); + try { + return Files.list(tempFolder) + .filter(f -> f.getFileName().toString().startsWith("apm-")) + .sorted() + .collect(Collectors.toList()); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Test + void shouldNotDeleteProvidedFiles() throws Exception { + // when an existing file is provided to the profiler, we should not delete it + // unlike the temporary files that are created by profiler itself + + setupProfiler(true); + profiler.stop(); + + Path tempFile1 = Files.createTempFile("apm-provided", "test.bin"); + Path tempFile2 = Files.createTempFile("apm-provided", "test.jfr"); + + SamplingProfiler otherProfiler = + new SamplingProfiler(tracer, new FixedNanoClock(), tempFile1.toFile(), tempFile2.toFile()); + + otherProfiler.start(tracer); + awaitProfilerStarted(otherProfiler); + otherProfiler.stop(); + + assertThat(tempFile1).exists(); + assertThat(tempFile2).exists(); + } + + @Test + void testStartCommand() { + setupProfiler(true); + assertThat(profiler.createStartCommand()) + .isEqualTo("start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0"); + doReturn(false).when(profilingConfig).isProfilingLoggingEnabled(); + assertThat(profiler.createStartCommand()) + .isEqualTo( + "start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0,log=none"); + doReturn(TimeDuration.of("10ms")).when(profilingConfig).getSamplingInterval(); + doReturn(14).when(profilingConfig).getAsyncProfilerSafeMode(); + assertThat(profiler.createStartCommand()) + .isEqualTo( + "start,jfr,event=wall,cstack=n,interval=10ms,filter,file=null,safemode=14,log=none"); + } + + @Test + void testProfileTransaction() throws Exception { + setupProfiler(true); + awaitProfilerStarted(profiler); + + Transaction transaction = tracer.startRootTransaction(null).withName("transaction"); + try (Scope scope = transaction.activateInScope()) { + // makes sure that the rest will be captured by another profiling session + // this tests that restoring which threads to profile works + Thread.sleep(600); + assertThat(profiler.isProfilingActiveOnThread(Thread.currentThread())).isTrue(); + aInferred(transaction); + } finally { + transaction.end(); + } + + await() + .pollDelay(10, TimeUnit.MILLISECONDS) + .timeout(5000, TimeUnit.MILLISECONDS) + .untilAsserted(() -> assertThat(reporter.getSpans()).hasSize(5)); + + Optional testProfileTransaction = + reporter.getSpans().stream() + .filter(s -> s.getNameAsString().equals("SamplingProfilerTest#testProfileTransaction")) + .findAny(); + assertThat(testProfileTransaction).isPresent(); + assertThat(testProfileTransaction.get().isChildOf(transaction)).isTrue(); + + Optional inferredSpanA = + reporter.getSpans().stream() + .filter(s -> s.getNameAsString().equals("SamplingProfilerTest#aInferred")) + .findAny(); + assertThat(inferredSpanA).isPresent(); + assertThat(inferredSpanA.get().isChildOf(testProfileTransaction.get())).isTrue(); + + Optional explicitSpanB = + reporter.getSpans().stream().filter(s -> s.getNameAsString().equals("bExplicit")).findAny(); + assertThat(explicitSpanB).isPresent(); + assertThat(explicitSpanB.get().isChildOf(inferredSpanA.get())).isTrue(); + + Optional inferredSpanC = + reporter.getSpans().stream() + .filter(s -> s.getNameAsString().equals("SamplingProfilerTest#cInferred")) + .findAny(); + assertThat(inferredSpanC).isPresent(); + assertThat(inferredSpanC.get().isChildOf(explicitSpanB.get())).isTrue(); + + Optional inferredSpanD = + reporter.getSpans().stream() + .filter(s -> s.getNameAsString().equals("SamplingProfilerTest#dInferred")) + .findAny(); + assertThat(inferredSpanD).isPresent(); + assertThat(inferredSpanD.get().isChildOf(inferredSpanC.get())).isTrue(); + } + + @Test + @DisabledForJreRange(max = JRE.JAVA_20) + void testVirtualThreadsExcluded() throws Exception { + setupProfiler(true); + awaitProfilerStarted(profiler); + + AtomicReference profilingActive = new AtomicReference<>(); + Runnable task = + () -> { + Transaction transaction = tracer.startRootTransaction(null).withName("transaction"); + try (Scope scope = transaction.activateInScope()) { + // makes sure that the rest will be captured by another profiling session + // this tests that restoring which threads to profile works + try { + Thread.sleep(600); + } catch (Exception e) { + throw new RuntimeException(e); + } + profilingActive.set(profiler.isProfilingActiveOnThread(Thread.currentThread())); + } finally { + transaction.end(); + } + }; + + Method startVirtualThread = Thread.class.getMethod("startVirtualThread", Runnable.class); + Thread virtual = (Thread) startVirtualThread.invoke(null, task); + virtual.join(); + + assertThat(profilingActive.get()).isFalse(); + } + + @Test + void testPostProcessingDisabled() throws Exception { + setupProfiler(true); + doReturn(false).when(profilingConfig).isPostProcessingEnabled(); + awaitProfilerStarted(profiler); + + Transaction transaction = tracer.startRootTransaction(null).withName("transaction"); + try (Scope scope = transaction.activateInScope()) { + // makes sure that the rest will be captured by another profiling session + // this tests that restoring which threads to profile works + Thread.sleep(600); + aInferred(transaction); + } finally { + transaction.end(); + } + + await() + .pollDelay(10, TimeUnit.MILLISECONDS) + .timeout(5000, TimeUnit.MILLISECONDS) + .untilAsserted(() -> assertThat(reporter.getSpans()).hasSize(1)); + + Optional explicitSpanB = + reporter.getSpans().stream().filter(s -> s.getNameAsString().equals("bExplicit")).findAny(); + assertThat(explicitSpanB).isPresent(); + assertThat(explicitSpanB.get().isChildOf(transaction)).isTrue(); + } + + private void aInferred(Transaction transaction) throws Exception { + Span span = transaction.createSpan().withName("bExplicit").withType("custom"); + try (Scope spanScope = span.activateInScope()) { + cInferred(); + } finally { + span.end(); + } + Thread.sleep(50); + } + + private void cInferred() throws Exception { + dInferred(); + Thread.sleep(50); + } + + private void dInferred() throws Exception { + Thread.sleep(50); + } + + private void setupProfiler(boolean enabled) { + reporter = new MockReporter(); + ConfigurationRegistry config = SpyConfiguration.createSpyConfig(); + profilingConfig = config.getConfig(ProfilingConfiguration.class); + + doReturn(List.of(WildcardMatcher.valueOf(getClass().getName()))) + .when(profilingConfig) + .getIncludedClasses(); + doReturn(enabled).when(profilingConfig).isProfilingEnabled(); + doReturn(TimeDuration.of("500ms")).when(profilingConfig).getProfilingDuration(); + doReturn(TimeDuration.of("500ms")).when(profilingConfig).getProfilingInterval(); + doReturn(TimeDuration.of("5ms")).when(profilingConfig).getSamplingInterval(); + tracer = MockTracer.createRealTracer(reporter, config); + profiler = tracer.getLifecycleListener(ProfilingFactory.class).getProfiler(); + } + + private static void awaitProfilerStarted(SamplingProfiler profiler) { + // ensure profiler is initialized + await() + .pollDelay(10, TimeUnit.MILLISECONDS) + .timeout(6000, TimeUnit.MILLISECONDS) + .until(() -> profiler.getProfilingSessions() > 1); + } + + private static void silentDeleteFile(Path f) { + try { + Files.delete(f); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java new file mode 100644 index 000000000..ddf7c4ed8 --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ThreadMatcherTest { + + private final ThreadMatcher threadMatcher = new ThreadMatcher(); + + @Test + void testLookup() { + ArrayList threads = new ArrayList<>(); + threadMatcher.forEachThread( + new ThreadMatcher.NonCapturingPredicate() { + @Override + public boolean test(Thread thread, Void state) { + return thread.getId() == Thread.currentThread().getId(); + } + }, + null, + new ThreadMatcher.NonCapturingConsumer>() { + @Override + public void accept(Thread thread, List state) { + state.add(thread); + } + }, + threads); + assertThat(threads).isEqualTo(List.of(Thread.currentThread())); + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java new file mode 100644 index 000000000..4f940ce67 --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java @@ -0,0 +1,66 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.asyncprofiler; + +import static co.elastic.apm.agent.profiler.asyncprofiler.AsyncProfiler.SAFEMODE_SYSTEM_PROPERTY_NAME; +import static org.assertj.core.api.Assertions.assertThat; + +import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; +import java.io.File; +import java.io.FilenameFilter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; + +@DisabledOnOs(OS.WINDOWS) +@DisabledOnAppleSilicon +public class AsyncProfilerTest { + + @BeforeEach + void setUp() { + AsyncProfiler.reset(); + } + + @Test + void testShouldCopyLibToTempDirectory() { + String defaultTempDirectory = System.getProperty("java.io.tmpdir"); + AsyncProfiler.getInstance(defaultTempDirectory, 0); + assertThat(Integer.valueOf(System.getProperty(SAFEMODE_SYSTEM_PROPERTY_NAME))).isEqualTo(0); + + File libDirectory = new File(defaultTempDirectory); + File[] libasyncProfilers = libDirectory.listFiles(getLibasyncProfilerFilenameFilter()); + assertThat(libasyncProfilers).hasSizeGreaterThanOrEqualTo(1); + } + + @Test + void testShouldCopyLibToSpecifiedDirectory(@TempDir File nonDefaultTempDirectory) { + AsyncProfiler.getInstance(nonDefaultTempDirectory.getAbsolutePath(), 6); + assertThat(Integer.valueOf(System.getProperty(SAFEMODE_SYSTEM_PROPERTY_NAME))).isEqualTo(6); + + File[] libasyncProfilers = + nonDefaultTempDirectory.listFiles(getLibasyncProfilerFilenameFilter()); + assertThat(libasyncProfilers).hasSizeGreaterThanOrEqualTo(1); + } + + private FilenameFilter getLibasyncProfilerFilenameFilter() { + return (dir, name) -> name.startsWith("libasyncProfiler") && name.endsWith(".so"); + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java new file mode 100644 index 000000000..c8834ebac --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java @@ -0,0 +1,224 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.asyncprofiler; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Set; +import java.util.zip.GZIPInputStream; +import javax.annotation.Nullable; +import one.profiler.AsyncProfiler; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.kohsuke.github.GHAsset; +import org.kohsuke.github.GHRelease; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.PagedIterable; + +/** + * This test class is disabled by default. It is used as a utility for manually upgrading async + * profiler - update the {@link #TARGET_VERSION} and run on a POSIX-compatible file system. + */ +@Disabled +public class AsyncProfilerUpgrader { + + static final String TARGET_VERSION = "1.8.8"; + static final String TAR_GZ_FILE_EXTENSION = ".tar.gz"; + static final String COMMON_BINARY_FILE_NAME = "libasyncProfiler.so"; + + static final String[] USED_ARTIFACTS = { + "linux-aarch64", "linux-arm", "linux-x64", "linux-x86", "macos-x64" + }; + + @Test + void updateAsyncProfilerBinaries() throws Exception { + GitHub github = GitHub.connectAnonymously(); + GHRepository repository = github.getRepository("jvm-profiling-tools/async-profiler"); + GHRelease release = repository.getReleaseByTagName("v" + TARGET_VERSION); + PagedIterable releaseAssets = release.listAssets(); + Path downloadDirPath = + Files.createTempDirectory(String.format("AsyncProfiler_%s_", TARGET_VERSION)); + for (GHAsset releaseAsset : releaseAssets) { + if (releaseAsset.getContentType().equals("application/x-gzip")) { + downloadAndReplaceBinary( + releaseAsset.getBrowserDownloadUrl(), + releaseAsset.getName(), + downloadDirPath, + releaseAsset.getSize()); + } + } + + // test we are now using the right version + Path thisOsLib = + getBinariesResourceDir() + .resolve( + co.elastic.apm.agent.profiler.asyncprofiler.AsyncProfiler.getLibraryFileName() + + ".so"); + AsyncProfiler asyncProfiler = AsyncProfiler.getInstance(thisOsLib.toString()); + assertThat(asyncProfiler.getVersion()).isEqualTo(TARGET_VERSION); + } + + private void downloadAndReplaceBinary( + String ghAssetDownloadUrl, String ghAssetName, Path targetDownloadDir, long expectedSize) + throws Exception { + String artifactNamePattern = null; + for (String artifact : USED_ARTIFACTS) { + if (ghAssetName.contains(artifact)) { + artifactNamePattern = artifact; + break; + } + } + if (artifactNamePattern == null) { + System.out.println( + ghAssetName + " is not within the list of used artifacts, skipping download"); + return; + } + Path targetDownloadPath = targetDownloadDir.resolve(ghAssetName); + System.out.println( + String.format( + "Downloading from %s into %s and extracting binary", ghAssetName, targetDownloadDir)); + Path localBinaryPath = + downloadAndExtractBinary(ghAssetDownloadUrl, targetDownloadPath, expectedSize); + assertThat(localBinaryPath) + .describedAs("Failed to download and extract binary file from " + ghAssetDownloadUrl) + .isNotNull(); + System.out.println( + String.format("Binary file for %s was extracted into %s", ghAssetName, localBinaryPath)); + replaceBinary(localBinaryPath, artifactNamePattern); + } + + @Nullable + private Path downloadAndExtractBinary( + String ghAssetDownloadUrl, Path targetDownloadPath, long expectedSize) throws IOException { + System.out.println("Downloading from " + ghAssetDownloadUrl); + URLConnection assetUrlConnection = new URL(ghAssetDownloadUrl).openConnection(); + long actualSize; + try (InputStream in = assetUrlConnection.getInputStream()) { + actualSize = Files.copy(in, targetDownloadPath); + } + assertThat(actualSize).isEqualTo(expectedSize); + return extractBinaryFileFromArchive(targetDownloadPath); + } + + @Nullable + private Path extractBinaryFileFromArchive(Path assetArchivePath) throws IOException { + String archiveFileName = assetArchivePath.getFileName().toString(); + if (!archiveFileName.endsWith(TAR_GZ_FILE_EXTENSION)) { + throw new IllegalArgumentException( + String.format( + "Cannot extract %s - expecting a path to a %s file", + archiveFileName, TAR_GZ_FILE_EXTENSION)); + } + + Path assetDirPath = assetArchivePath.getParent(); + if (!Files.exists(assetDirPath)) { + Files.createDirectory(assetDirPath); + } + String extractedDirName = + archiveFileName.substring(0, archiveFileName.length() - TAR_GZ_FILE_EXTENSION.length()); + Path extractedDirPath = assetDirPath.resolve(extractedDirName); + if (!Files.exists(extractedDirPath)) { + Files.createDirectory(extractedDirPath); + } + + Path binaryFilePath = null; + try (InputStream fis = Files.newInputStream(assetArchivePath); + GZIPInputStream gis = new GZIPInputStream(fis); + ArchiveInputStream ais = new TarArchiveInputStream(gis)) { + + ArchiveEntry entry; + while ((entry = ais.getNextEntry()) != null) { + if (!ais.canReadEntryData(entry)) { + throw new IllegalStateException("Cannot read an archive entry - " + entry.getName()); + } + if (entry.getName().endsWith(COMMON_BINARY_FILE_NAME)) { + Path filePath = extractedDirPath.resolve(COMMON_BINARY_FILE_NAME); + Files.copy(ais, filePath); + binaryFilePath = filePath; + } + } + } + return binaryFilePath; + } + + /** + * Replaces an existing binary file with its downloaded counterpart. + * + *

    NOTE: when replacing the existing binary file, this method attempts to apply the current + * binary file's permissions to the one replacing it, assuming the underlying file system is + * POSIX-compatible. If this is not the case, and error will occur + * + * @param downloadedArtifact the path to the downloaded binary file + * @param artifactName the name of the artifact to replace, see {@link #USED_ARTIFACTS} + * @throws Exception thrown when an error occurs while trying to replace, or when running on non + * POSIX file system + */ + private void replaceBinary(Path downloadedArtifact, String artifactName) throws Exception { + if (!downloadedArtifact.toString().contains(artifactName)) { + throw new IllegalArgumentException( + String.format( + "the provided path for the downloaded artifact [%s] must " + + "be of a file containing the provided artifact name: %s", + downloadedArtifact, artifactName)); + } + + Path binariesResourceDir = getBinariesResourceDir(); + String binaryResourceName = String.format("libasyncProfiler-%s.so", artifactName); + Path binaryResourcePath = binariesResourceDir.resolve(binaryResourceName); + if (!Files.exists(binaryResourcePath)) { + throw new IllegalStateException( + String.format("Expected binary file does not exist: %s", binaryResourcePath.toString())); + } + System.out.println( + String.format("Replacing %s with %s", binaryResourcePath, downloadedArtifact)); + Set posixFilePermissions = + Files.getPosixFilePermissions(binaryResourcePath); + Files.move(downloadedArtifact, binaryResourcePath, StandardCopyOption.REPLACE_EXISTING); + Files.setPosixFilePermissions(binaryResourcePath, posixFilePermissions); + } + + private Path getBinariesResourceDir() throws URISyntaxException { + // /apm-agent-plugins/apm-profiling-plugin/target/classes/asyncprofiler + Path asyncProfilerTestResourcePath = + Paths.get(AsyncProfilerUpgrader.class.getResource("/asyncprofiler").toURI()); + // /apm-agent-plugins/apm-profiling-plugin/target/classes/asyncprofiler + Path pluginRootDir = asyncProfilerTestResourcePath.getParent().getParent().getParent(); + // We are looking for // + // /apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler + return pluginRootDir + .resolve("src") + .resolve("main") + .resolve("resources") + .resolve("asyncprofiler"); + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java new file mode 100644 index 000000000..c73219285 --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler.asyncprofiler; + +import static co.elastic.apm.agent.common.util.WildcardMatcher.caseSensitiveMatcher; +import static org.assertj.core.api.Assertions.assertThat; + +import co.elastic.apm.agent.impl.transaction.StackFrame; +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class JfrParserTest { + + private static final int MAX_STACK_DEPTH = 4; + + @Test + void name() throws Exception { + // using the smallest prime number possible for the buffer + // should trigger most edge cases in the buffer being exhausted + JfrParser jfrParser = new JfrParser(ByteBuffer.allocate(113), ByteBuffer.allocate(113)); + + File file = + Paths.get(JfrParserTest.class.getClassLoader().getResource("recording.jfr").toURI()) + .toFile(); + + jfrParser.parse(file, List.of(), List.of(caseSensitiveMatcher("co.elastic.apm.*"))); + AtomicInteger stackTraces = new AtomicInteger(); + ArrayList stackFrames = new ArrayList<>(); + jfrParser.consumeStackTraces( + (threadId, stackTraceId, nanoTime) -> { + jfrParser.resolveStackTrace(stackTraceId, true, stackFrames, MAX_STACK_DEPTH); + if (!stackFrames.isEmpty()) { + stackTraces.incrementAndGet(); + assertThat(stackFrames.get(stackFrames.size() - 1).getMethodName()) + .isEqualTo("testProfileTransaction"); + assertThat(stackFrames).hasSizeLessThanOrEqualTo(MAX_STACK_DEPTH); + } + stackFrames.clear(); + }); + assertThat(stackTraces.get()).isEqualTo(97); + } +} diff --git a/inferred-spans/src/test/resources/recording.jfr b/inferred-spans/src/test/resources/recording.jfr new file mode 100644 index 0000000000000000000000000000000000000000..f8122c798a6c0ff6be82f604c5f7c0cda96ca370 GIT binary patch literal 59679 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

    cS>||MBD11 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 literal 0 HcmV?d00001 From 741c918898553dcdb1235f237ac09872c16cf468 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Thu, 23 Nov 2023 12:22:39 +0100 Subject: [PATCH 02/18] Ignore OS X DS_Store files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6a205ec4c..ec889934a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ examples/**/build/ *.ipr *.iws out/ + +# OS X +**/.DS_Store From 624202a23bc9b8f67f7de8d7953da5b28f9e3db8 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Thu, 23 Nov 2023 12:31:00 +0100 Subject: [PATCH 03/18] Implemented inferred spans otel plugin --- build.gradle | 3 + inferred-spans/build.gradle.kts | 41 + .../elastic/apm/otel/profiler/CallTree.java | 487 +++---- .../profiler/InferredSpansConfiguration.java | 119 ++ .../otel/profiler/InferredSpansProcessor.java | 98 ++ .../InferredSpansProcessorBuilder.java | 195 +++ .../elastic/apm/otel/profiler/NanoClock.java | 14 +- .../profiler/ProfilingActivationListener.java | 129 +- .../otel/profiler/ProfilingConfiguration.java | 266 ---- .../apm/otel/profiler/ProfilingFactory.java | 55 - .../apm/otel/profiler/SamplingProfiler.java | 736 ++++------ .../otel/profiler/SpanAnchoredNanoClock.java | 62 + .../elastic/apm/otel/profiler/StackFrame.java | 86 ++ .../apm/otel/profiler/SystemNanoClock.java | 26 - .../apm/otel/profiler/ThreadMatcher.java | 14 +- .../apm/otel/profiler/TraceContext.java | 144 ++ .../profiler/asyncprofiler/AsyncProfiler.java | 92 +- .../profiler/asyncprofiler/BufferedFile.java | 138 +- .../profiler/asyncprofiler/JfrParser.java | 157 +- .../asyncprofiler/ResourceExtractionUtil.java | 139 ++ .../profiler/asyncprofiler/package-info.java | 22 - .../profiler/collections/CollectionUtil.java | 45 +- .../otel/profiler/collections/Hashing.java | 25 +- .../profiler/collections/Int2IntHashMap.java | 222 ++- .../collections/Int2ObjectHashMap.java | 176 ++- .../profiler/collections/IntIntConsumer.java | 24 +- .../collections/Long2LongHashMap.java | 211 ++- .../collections/Long2ObjectHashMap.java | 177 ++- .../profiler/collections/LongHashSet.java | 167 ++- .../otel/profiler/collections/LongList.java | 123 ++ .../collections/LongLongConsumer.java | 24 +- .../profiler/collections/package-info.java | 2 +- .../otel/profiler/config/WildcardMatcher.java | 373 +++++ .../apm/otel/profiler/package-info.java | 22 - .../profiler/pooling/AbstractObjectPool.java | 75 + .../apm/otel/profiler/pooling/Allocator.java | 14 + .../apm/otel/profiler/pooling/ObjectPool.java | 34 + .../pooling/QueueBasedObjectPool.java | 74 + .../apm/otel/profiler/pooling/Recyclable.java | 10 + .../apm/otel/profiler/pooling/Resetter.java | 36 + .../apm/otel/profiler/util/ByteUtils.java | 25 + .../apm/otel/profiler/util/HexUtils.java | 70 + .../apm/otel/profiler/util/ThreadUtils.java | 36 + .../otel/profiler/CallTreeSpanifyTest.java | 218 +-- .../apm/otel/profiler/CallTreeTest.java | 1288 +++++++++-------- .../apm/otel/profiler/FixedNanoClock.java | 27 +- .../apm/otel/profiler/ProfilerTestSetup.java | 57 + .../profiler/SamplingProfilerQueueTest.java | 51 +- .../otel/profiler/SamplingProfilerReplay.java | 66 +- .../otel/profiler/SamplingProfilerTest.java | 285 ++-- .../apm/otel/profiler/ThreadMatcherTest.java | 29 +- .../asyncprofiler/AsyncProfilerTest.java | 10 +- .../asyncprofiler/AsyncProfilerUpgrader.java | 91 +- .../profiler/asyncprofiler/JfrParserTest.java | 33 +- .../profiler/util/DisabledOnAppleSilicon.java | 20 + .../util/DisabledOnAppleSiliconCondition.java | 29 + .../src/test/resources/logging.properties | 6 + settings.gradle.kts | 1 + 58 files changed, 4602 insertions(+), 2597 deletions(-) create mode 100644 inferred-spans/build.gradle.kts create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java delete mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingConfiguration.java delete mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingFactory.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java delete mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SystemNanoClock.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java delete mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/package-info.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java delete mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/package-info.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java create mode 100644 inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java rename inferred-spans/src/{main => test}/java/co/elastic/apm/otel/profiler/FixedNanoClock.java (66%) create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java create mode 100644 inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java create mode 100644 inferred-spans/src/test/resources/logging.properties diff --git a/build.gradle b/build.gradle index 414a8c0c6..45e06c916 100644 --- a/build.gradle +++ b/build.gradle @@ -8,9 +8,11 @@ version '0.0.1-SNAPSHOT' defaultTasks("agent:assemble") subprojects { + group = rootProject.group version = rootProject.version apply plugin: "java" + apply plugin: "maven-publish" //Allows to run ./gradle publishToMavenLocal for easy local testing apply plugin: "com.diffplug.spotless" ext { @@ -23,6 +25,7 @@ subprojects { } repositories { + mavenLocal() mavenCentral() maven { name = "sonatype" diff --git a/inferred-spans/build.gradle.kts b/inferred-spans/build.gradle.kts new file mode 100644 index 000000000..d921d61a9 --- /dev/null +++ b/inferred-spans/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + `java-library` +} + +dependencies { + compileOnly("io.opentelemetry:opentelemetry-sdk") + compileOnly("com.google.code.findbugs:jsr305:3.0.0") + implementation("com.lmax:disruptor:3.4.4") + implementation("org.jctools:jctools-core:4.0.1") + implementation("com.blogspot.mydailyjava:weak-lock-free:0.18") + + testCompileOnly("com.google.code.findbugs:jsr305:3.0.0") + testImplementation("io.opentelemetry:opentelemetry-sdk") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("org.kohsuke:github-api:1.133") + testImplementation("org.awaitility:awaitility:4.2.0") + testImplementation("org.apache.commons:commons-compress:1.21") + testImplementation("tools.profiler:async-profiler:1.8.3") +} + +tasks.withType().all { + jvmArgs("-Djava.util.logging.config.file="+sourceSets.test.get().output.resourcesDir+"/logging.properties") +} + + +publishing { + publications { + create("maven") { + from(components["java"]) + versionMapping { + usage("java-api") { + fromResolutionOf("runtimeClasspath") + } + usage("java-runtime") { + fromResolutionResult() + } + } + } + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java index 0133e09ac..7863af919 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java @@ -16,71 +16,91 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; - -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.impl.transaction.Span; -import co.elastic.apm.agent.impl.transaction.StackFrame; -import co.elastic.apm.agent.impl.transaction.TraceContext; -import co.elastic.apm.agent.profiler.collections.LongHashSet; -import co.elastic.apm.agent.sdk.internal.collections.LongList; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; -import co.elastic.apm.agent.tracer.pooling.ObjectPool; -import co.elastic.apm.agent.tracer.pooling.Recyclable; +package co.elastic.apm.otel.profiler; + + +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.WARNING; + +import co.elastic.apm.otel.profiler.collections.LongHashSet; +import co.elastic.apm.otel.profiler.util.HexUtils; +import co.elastic.apm.otel.profiler.collections.LongList; +import co.elastic.apm.otel.profiler.pooling.ObjectPool; +import co.elastic.apm.otel.profiler.pooling.Recyclable; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; import javax.annotation.Nullable; /** * Converts a sequence of stack traces into a tree structure of method calls. - * *
      *             count
      *  b b     a      4
      * aaaa ──► ├─b    1
      *          └─b    1
      * 
    - * - *

    It also stores information about which span is the parent of a particular call tree node, + *

    + * It also stores information about which span is the parent of a particular call tree node, * based on which span has been {@linkplain ElasticApmTracer#getActive() active} at that time. - * - *

    This allows to {@linkplain Root#spanify() infer spans from the call tree} which have the - * correct parent/child relationships with the regular spans. + *

    + *

    + * This allows to {@linkplain Root#spanify() infer spans from the call tree} which have the correct parent/child relationships + * with the regular spans. + *

    */ +@SuppressWarnings("javadoc") public class CallTree implements Recyclable { private static final int INITIAL_CHILD_SIZE = 2; - @Nullable private CallTree parent; + static final AttributeKey IS_CHILD_ATTRIBUTE_KEY = + AttributeKey.booleanKey("elastic.is_child"); + + private static final Attributes CHILD_LINK_ATTRIBUTES = Attributes.builder() + .put(IS_CHILD_ATTRIBUTE_KEY, true) + .build(); + + static final AttributeKey STACKTRACE_ATTRIBUTE_KEY = + AttributeKey.stringKey("code.stacktrace"); + @Nullable + private CallTree parent; protected int count; private List children = new ArrayList<>(INITIAL_CHILD_SIZE); - @Nullable private StackFrame frame; + @Nullable + private StackFrame frame; protected long start; private long lastSeen; private boolean ended; private long activationTimestamp = -1; - /** - * The context of the transaction or span which is the direct parent of this call tree node. Used - * in {@link #spanify} to override the parent. + * The context of the transaction or span which is the direct parent of this call tree node. + * Used in {@link #spanify} to override the parent. */ - @Nullable private TraceContext activeContextOfDirectParent; - + @Nullable + private TraceContext activeContextOfDirectParent; private long deactivationTimestamp = -1; private boolean isSpan; private int depth; - /** * @see co.elastic.apm.agent.impl.transaction.AbstractSpan#childIds */ - @Nullable private LongList childIds; - - @Nullable private LongList maybeChildIds; + @Nullable + private LongList childIds; + @Nullable + private LongList maybeChildIds; - public CallTree() {} + public CallTree() { + } public void set(@Nullable CallTree parent, StackFrame frame, long nanoTime) { this.parent = parent; @@ -116,8 +136,8 @@ public void activation(TraceContext traceContext, long activationTimestamp) { this.activationTimestamp = activationTimestamp; } - protected void handleDeactivation( - TraceContext deactivatedSpan, long activationTimestamp, long deactivationTimestamp) { + protected void handleDeactivation(TraceContext deactivatedSpan, long activationTimestamp, + long deactivationTimestamp) { if (deactivatedSpan.idEquals(activeContextOfDirectParent)) { this.deactivationTimestamp = deactivationTimestamp; } else { @@ -127,8 +147,7 @@ protected void handleDeactivation( } } // if an actual child span is deactivated after this call tree node has ended - // it means that this node has actually ended at least at the same point, if not after, the - // actual span has been deactivated + // it means that this node has actually ended at least at the same point, if not after, the actual span has been deactivated // // [a(inferred)] ─► [a(inferred) ] ← set end timestamp to timestamp of deactivation of b // └─[b(actual) ] └─[b(actual) ] @@ -146,45 +165,29 @@ private boolean happenedAfter(long timestamp) { return lastSeen < timestamp; } - public static CallTree.Root createRoot( - ObjectPool rootPool, - byte[] traceContext, - @Nullable String serviceName, - @Nullable String serviceVersion, + public static CallTree.Root createRoot(ObjectPool rootPool, byte[] traceContext, long nanoTime) { CallTree.Root root = rootPool.createInstance(); - root.set(traceContext, serviceName, serviceVersion, nanoTime); + root.set(traceContext, nanoTime); return root; } /** - * Adds a single stack trace to the call tree which either updates the {@link #lastSeen} timestamp - * of an existing call tree node, {@linkplain #end ends} a node, or {@linkplain #addChild adds a - * new child}. + * Adds a single stack trace to the call tree which either updates the {@link #lastSeen} timestamp of an existing call tree node, + * {@linkplain #end ends} a node, or {@linkplain #addChild adds a new child}. * * @param stackFrames the stack trace which is iterated over in reverse order * @param index the current index of {@code stackFrames} - * @param activeSpan the trace context of the currently {@linkplain ElasticApmTracer#getActive()} - * active transaction/span + * @param activeSpan the trace context of the currently {@linkplain ElasticApmTracer#getActive()} active transaction/span * @param activationTimestamp the timestamp of when {@code traceContext} has been activated * @param nanoTime the timestamp of when this stack trace has been recorded - * @param callTreePool - * @param minDurationNs - * @param root */ - protected CallTree addFrame( - List stackFrames, - int index, - @Nullable TraceContext activeSpan, - long activationTimestamp, - long nanoTime, - ObjectPool callTreePool, - long minDurationNs, - Root root) { + protected CallTree addFrame(List stackFrames, int index, + @Nullable TraceContext activeSpan, long activationTimestamp, long nanoTime, + ObjectPool callTreePool, long minDurationNs, Root root) { count++; lastSeen = nanoTime; - // c ee ← traceContext not set - they are not a child of the active span but the frame - // below them + // c ee ← traceContext not set - they are not a child of the active span but the frame below them // bbb dd ← traceContext set // ------ ← all new CallTree during this period should have the traceContext set // a aaaaaa a @@ -193,8 +196,7 @@ protected CallTree addFrame( // this branch is already aware of the activation // this means the provided activeSpan is not a direct parent of new child nodes - if (activeSpan != null - && this.activeContextOfDirectParent != null + if (activeSpan != null && this.activeContextOfDirectParent != null && this.activeContextOfDirectParent.idEquals(activeSpan)) { activeSpan = null; } @@ -209,42 +211,16 @@ protected CallTree addFrame( final StackFrame frame = stackFrames.get(--index); if (lastChild != null) { if (!lastChild.isEnded() && frame.equals(lastChild.frame)) { - topOfStack = - lastChild.addFrame( - stackFrames, - index, - activeSpan, - activationTimestamp, - nanoTime, - callTreePool, - minDurationNs, - root); + topOfStack = lastChild.addFrame(stackFrames, index, activeSpan, activationTimestamp, + nanoTime, callTreePool, minDurationNs, root); endChild = false; } else { - topOfStack = - addChild( - frame, - stackFrames, - index, - activeSpan, - activationTimestamp, - nanoTime, - callTreePool, - minDurationNs, - root); + topOfStack = addChild(frame, stackFrames, index, activeSpan, activationTimestamp, + nanoTime, callTreePool, minDurationNs, root); } } else { - topOfStack = - addChild( - frame, - stackFrames, - index, - activeSpan, - activationTimestamp, - nanoTime, - callTreePool, - minDurationNs, - root); + topOfStack = addChild(frame, stackFrames, index, activeSpan, activationTimestamp, nanoTime, + callTreePool, minDurationNs, root); } } if (lastChild != null && !lastChild.isEnded() && endChild) { @@ -255,8 +231,8 @@ protected CallTree addFrame( } /** - * This method is called when we know for sure that the maybe child ids are actually belonging to - * this call tree. This is the case after we've seen another frame represented by this call tree. + * This method is called when we know for sure that the maybe child ids are actually belonging to this call tree. + * This is the case after we've seen another frame represented by this call tree. * * @see #addMaybeChildId(long) */ @@ -272,24 +248,17 @@ private void transferMaybeChildIdsToChildIds() { } } - private CallTree addChild( - StackFrame frame, - List stackFrames, - int index, - @Nullable TraceContext traceContext, - long activationTimestamp, - long nanoTime, - ObjectPool callTreePool, - long minDurationNs, - Root root) { + private CallTree addChild(StackFrame frame, List stackFrames, int index, + @Nullable TraceContext traceContext, long activationTimestamp, long nanoTime, + ObjectPool callTreePool, long minDurationNs, Root root) { CallTree callTree = callTreePool.createInstance(); callTree.set(this, frame, nanoTime); if (traceContext != null) { callTree.activation(traceContext, activationTimestamp); } children.add(callTree); - return callTree.addFrame( - stackFrames, index, null, activationTimestamp, nanoTime, callTreePool, minDurationNs, root); + return callTree.addFrame(stackFrames, index, null, activationTimestamp, nanoTime, callTreePool, + minDurationNs, root); } long getDurationUs() { @@ -329,8 +298,7 @@ protected void end(ObjectPool pool, long minDurationNs, Root root) { if (parent != null) { // we know there's always exactly one activation in the parent's childIds // that needs to be transferred to this call tree node - // in the above example, 1's child id would be first transferred from a to b and then from b - // to c + // in the above example, 1's child id would be first transferred from a to b and then from b to c // this ensures that the UI knows that c is the parent of 1 parent.giveLastChildIdTo(this); } @@ -373,8 +341,7 @@ private boolean isFasterThan(long minDurationNs) { } private boolean deactivationHappenedBeforeEnd() { - return activeContextOfDirectParent != null - && deactivationTimestamp > -1 + return activeContextOfDirectParent != null && deactivationTimestamp > -1 && lastSeen > deactivationTimestamp; } @@ -426,86 +393,126 @@ private void toString(Appendable out, int level) throws IOException { out.append(frame != null ? frame.getClassName() : "null") .append('.') .append(frame != null ? frame.getMethodName() : "null") - .append(' ') - .append(Integer.toString(count)) + .append(' ').append(Integer.toString(count)) .append('\n'); for (CallTree node : children) { node.toString(out, level + 1); } } - int spanify(CallTree.Root root, TraceContext parentContext) { + int spanify( + CallTree.Root root, + @Nullable Span parentSpan, + TraceContext parentContext, + NanoClock clock, + StringBuilder tempBuilder, + Tracer tracer + ) { int createdSpans = 0; if (activeContextOfDirectParent != null) { + parentSpan = null; parentContext = activeContextOfDirectParent; } Span span = null; if (!isPillar() || isLeaf()) { createdSpans++; - span = asSpan(root, parentContext); + span = asSpan(root, parentSpan, parentContext, tracer, clock, tempBuilder); this.isSpan = true; } List children = getChildren(); for (int i = 0, size = children.size(); i < size; i++) { - createdSpans += - children.get(i).spanify(root, span != null ? span.getTraceContext() : parentContext); - } - if (span != null) { - span.end(span.getTimestamp() + getDurationUs()); + createdSpans += children.get(i) + .spanify(root, span != null ? span : parentSpan, parentContext, clock, tempBuilder, + tracer); } return createdSpans; } - protected Span asSpan(Root root, TraceContext parentContext) { - transferMaybeChildIdsToChildIds(); - Span span = - parentContext - .createSpan(root.getEpochMicros(this.start)) - .withType("app") - .withSubtype("inferred"); + protected Span asSpan(Root root, @Nullable Span parentSpan, TraceContext parentContext, + Tracer tracer, NanoClock clock, + StringBuilder tempBuilder) { + + Context parentOtelCtx; + if (parentSpan != null) { + parentOtelCtx = Context.root().with(parentSpan); + } else { + tempBuilder.setLength(0); + parentOtelCtx = Context.root().with(Span.wrap(parentContext.toOtelSpanContext(tempBuilder))); + } + tempBuilder.setLength(0); String classFqn = frame.getClassName(); if (classFqn != null) { - span.appendToName( - classFqn, - co.elastic.apm.agent.tracer.AbstractSpan.PRIORITY_DEFAULT, - frame.getSimpleClassNameOffset(), - classFqn.length()); + tempBuilder.append(classFqn, frame.getSimpleClassNameOffset(), classFqn.length()); } else { - span.appendToName("null"); + tempBuilder.append("null"); } - span.appendToName("#"); - span.appendToName(frame.getMethodName()); - span.withChildIds(childIds); + tempBuilder.append("#"); + tempBuilder.append(frame.getMethodName()); - // we're not interested in the very bottom of the stack which contains things like accepting and - // handling connections - if (!root.rootContext.idEquals(parentContext)) { + transferMaybeChildIdsToChildIds(); + + SpanBuilder spanBuilder = tracer.spanBuilder(tempBuilder.toString()) + .setParent(parentOtelCtx) + .setStartTimestamp(clock.toEpochNanos(parentContext.getClockAnchor(), this.start), + TimeUnit.NANOSECONDS); + insertChildIdLinks(spanBuilder, Span.fromContext(parentOtelCtx).getSpanContext(), tempBuilder); + + // we're not interested in the very bottom of the stack which contains things like accepting and handling connections + if (parentSpan != null || !root.rootContext.idEquals(parentContext)) { // we're never spanifying the root assert this.parent != null; - List stackTrace = new ArrayList<>(); - this.parent.fillStackTrace(stackTrace); - span.setStackTrace(stackTrace); - } else { - span.setStackTrace(Collections.emptyList()); + tempBuilder.setLength(0); + this.parent.fillStackTrace(tempBuilder); + spanBuilder.setAttribute(STACKTRACE_ATTRIBUTE_KEY, tempBuilder.toString()); } + + Span span = spanBuilder.startSpan(); + span.end(clock.toEpochNanos(parentContext.getClockAnchor(), this.start + getDurationNs()), + TimeUnit.NANOSECONDS); return span; } - /** Fill in the stack trace up to the parent span */ - private void fillStackTrace(List stackTrace) { + private void insertChildIdLinks(SpanBuilder span, SpanContext parentContext, + StringBuilder tempBuilder) { + if (childIds == null || childIds.isEmpty()) { + return; + } + for (int i = 0; i < childIds.getSize(); i++) { + tempBuilder.setLength(0); + HexUtils.appendLongAsHex(childIds.get(i), tempBuilder); + SpanContext spanContext = SpanContext.create( + parentContext.getTraceId(), + tempBuilder.toString(), + parentContext.getTraceFlags(), + parentContext.getTraceState() + ); + span.addLink(spanContext, CHILD_LINK_ATTRIBUTES); + } + } + + /** + * Fill in the stack trace up to the parent span + */ + private void fillStackTrace(StringBuilder resultBuilder) { if (parent != null && !this.isSpan) { - stackTrace.add(frame); - parent.fillStackTrace(stackTrace); + if (resultBuilder.length() > 0) { + resultBuilder.append('\n'); + } + resultBuilder.append("at ") + .append(frame.getClassName()).append('.').append(frame.getMethodName()) + .append('('); + frame.appendFileName(resultBuilder); + resultBuilder.append(')'); + parent.fillStackTrace(resultBuilder); } } /** - * Recycles this subtree to the provided pool recursively. Note that this method ends by recycling - * {@code this} node (i.e. - this subtree root), which means that the caller of this method - * should make sure that no reference to this object is held anywhere. - * - *

    ALSO NOTE: MAKE SURE NOT TO CALL THIS METHOD FOR {@link CallTree.Root} INSTANCES. + * Recycles this subtree to the provided pool recursively. + * Note that this method ends by recycling {@code this} node (i.e. - this subtree root), which means that + * the caller of this method should make sure that no reference to this object is held anywhere. + *

    ALSO NOTE: MAKE SURE NOT TO CALL THIS METHOD FOR {@link CallTree.Root} INSTANCES.

    * * @param pool the pool to which all subtree nodes are to be recycled */ @@ -543,24 +550,25 @@ public void resetState() { } /** - * When a regular span is activated, we want it's {@link TraceContext#getId() span.id} to be added - * to the call tree that represents the {@linkplain CallTree.Root#topOfStack top of the stack} to - * ensure correct parent/child relationships via re-parenting (See also {@link Span#childIds}). - * - *

    However, the {@linkplain CallTree.Root#topOfStack current top of the stack} may turn out to - * not be the right target. Consider this example: - * + * When a regular span is activated, + * we want it's {@link TraceContext#getId() span.id} to be added to the call tree that represents the + * {@linkplain CallTree.Root#topOfStack top of the stack} to ensure correct parent/child relationships via re-parenting (See also {@link Span#childIds}). + *

    + * However, the {@linkplain CallTree.Root#topOfStack current top of the stack} may turn out to not be the right target. + * Consider this example: + *

    *
        * bb
        * aa aa
        *   1  1  ← activation
        * 
    - * - *

    We would add the id of span {@code 1} to {@code b}'s {@link #maybeChildIds}. But after - * seeing the next frame, we realize the {@code b} has already ended and that we should {@link - * #giveMaybeChildIdsTo} from {@code b} and give it to {@code a}. This logic is implemented in - * {@link CallTree.Root#addStackTrace}. After seeing another frame of {@code a}, we know that - * {@code 1} is really the child of {@code a}, so we {@link #transferMaybeChildIdsToChildIds()}. + *

    + * We would add the id of span {@code 1} to {@code b}'s {@link #maybeChildIds}. + * But after seeing the next frame, + * we realize the {@code b} has already ended and that we should {@link #giveMaybeChildIdsTo} from {@code b} and give it to {@code a}. + * This logic is implemented in {@link CallTree.Root#addStackTrace}. + * After seeing another frame of {@code a}, we know that {@code 1} is really the child of {@code a}, so we {@link #transferMaybeChildIdsToChildIds()}. + *

    * * @param id the child span id to add to this call tree element */ @@ -603,6 +611,7 @@ void giveChildIdsTo(CallTree giveTo) { this.childIds = null; } + void giveLastChildIdTo(CallTree giveTo) { if (childIds != null && !childIds.isEmpty()) { giveTo.addChildId(childIds.remove(childIds.getSize() - 1)); @@ -626,59 +635,55 @@ public int getDepth() { } /** - * A special kind of a {@link CallTree} node which represents the root of the call tree. This acts - * as the interface to the outside to add new nodes to the tree or to update existing ones by + * A special kind of a {@link CallTree} node which represents the root of the call tree. + * This acts as the interface to the outside to add new nodes to the tree or to update existing ones by * {@linkplain #addStackTrace adding stack traces}. */ public static class Root extends CallTree implements Recyclable { - private static final Logger logger = LoggerFactory.getLogger(Root.class); + private static final Logger logger = Logger.getLogger(Root.class.getName()); private static final StackFrame ROOT_FRAME = new StackFrame("root", "root"); - /** - * The context of the thread root, mostly a transaction or a span which got activated in an - * auxiliary thread + * The context of the thread root, + * mostly a transaction or a span which got activated in an auxiliary thread */ protected TraceContext rootContext; - /** - * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() - * active}. This is lazily deserialized from {@link #activeSpanSerialized} if there's an actual - * {@linkplain #addStackTrace stack trace} for this activation. + * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() active}. + * This is lazily deserialized from {@link #activeSpanSerialized} if there's an actual {@linkplain #addStackTrace stack trace} + * for this activation. + */ + @Nullable + private TraceContext activeSpan; + /** + * The timestamp of when {@link #activeSpan} got activated */ - @Nullable private TraceContext activeSpan; - - /** The timestamp of when {@link #activeSpan} got activated */ private long activationTimestamp = -1; - /** - * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() - * active}, in its {@linkplain TraceContext#serialize serialized} form. + * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() active}, + * in its {@linkplain TraceContext#serialize serialized} form. */ private byte[] activeSpanSerialized = new byte[TraceContext.SERIALIZED_LENGTH]; - - @Nullable private CallTree previousTopOfStack; - @Nullable private CallTree topOfStack; + @Nullable + private CallTree previousTopOfStack; + @Nullable + private CallTree topOfStack; private final LongHashSet activeSet = new LongHashSet(); - public Root(ElasticApmTracer tracer) { - this.rootContext = TraceContext.with64BitId(tracer); + public Root() { + this.rootContext = new TraceContext(); } - private void set( - byte[] traceContext, - @Nullable String serviceName, - @Nullable String serviceVersion, - long nanoTime) { + private void set(byte[] traceContext, long nanoTime) { super.set(null, ROOT_FRAME, nanoTime); - this.rootContext.deserialize(traceContext, serviceName, serviceVersion); + this.rootContext.deserialize(traceContext); setActiveSpan(traceContext, nanoTime); } public void setActiveSpan(byte[] activeSpanSerialized, long timestamp) { activationTimestamp = timestamp; - System.arraycopy( - activeSpanSerialized, 0, this.activeSpanSerialized, 0, activeSpanSerialized.length); + System.arraycopy(activeSpanSerialized, 0, this.activeSpanSerialized, 0, + activeSpanSerialized.length); this.activeSpan = null; } @@ -710,17 +715,15 @@ private boolean isAnyActive(@Nullable LongList spanIds) { } public void onDeactivation(byte[] deactivated, byte[] active, long timestamp) { - if (logger.isDebugEnabled() && !Arrays.equals(activeSpanSerialized, deactivated)) { - logger.warn("Illegal state: deactivating span that is not active"); + if (logger.isLoggable(FINE) && !Arrays.equals(activeSpanSerialized, deactivated)) { + logger.log(WARNING, "Illegal state: deactivating span that is not active"); } if (activeSpan != null) { handleDeactivation(activeSpan, activationTimestamp, timestamp); } - // else: activeSpan has not been materialized because no stack traces were added during this - // activation + // else: activeSpan has not been materialized because no stack traces were added during this activation setActiveSpan(active, timestamp); - // we're not interested in tracking nested activations that happen before we see the first - // stack trace + // we're not interested in tracking nested activations that happen before we see the first stack trace // that's because isNestedActivation is only called if topOfStack != null // this optimizes for the case where we have no stack traces for a fast executing transaction if (topOfStack != null) { @@ -729,45 +732,26 @@ public void onDeactivation(byte[] deactivated, byte[] active, long timestamp) { } } - public void addStackTrace( - ElasticApmTracer tracer, - List stackTrace, - long nanoTime, - ObjectPool callTreePool, - long minDurationNs) { - // only "materialize" trace context if there's actually an associated stack trace to the - // activation - // avoids allocating a TraceContext for very short activations which have no effect on the - // CallTree anyway + public void addStackTrace(List stackTrace, long nanoTime, + ObjectPool callTreePool, long minDurationNs) { + // only "materialize" trace context if there's actually an associated stack trace to the activation + // avoids allocating a TraceContext for very short activations which have no effect on the CallTree anyway boolean firstFrameAfterActivation = false; if (activeSpan == null) { firstFrameAfterActivation = true; - activeSpan = TraceContext.with64BitId(tracer); - activeSpan.deserialize( - activeSpanSerialized, rootContext.getServiceName(), rootContext.getServiceVersion()); + activeSpan = new TraceContext(); + activeSpan.deserialize(activeSpanSerialized); } previousTopOfStack = topOfStack; - topOfStack = - addFrame( - stackTrace, - stackTrace.size(), - activeSpan, - activationTimestamp, - nanoTime, - callTreePool, - minDurationNs, - this); - - // After adding the first frame after an activation, we can check if we added the child ids to - // the correct CallTree - // If the new top of stack is not a successor (a different branch vs just added nodes on the - // same branch) + topOfStack = addFrame(stackTrace, stackTrace.size(), activeSpan, activationTimestamp, + nanoTime, callTreePool, minDurationNs, this); + + // After adding the first frame after an activation, we can check if we added the child ids to the correct CallTree + // If the new top of stack is not a successor (a different branch vs just added nodes on the same branch) // we have to transfer the child ids of not yet deactivated spans to the new top of the stack. // See also CallTreeTest.testActivationAfterMethodEnds and following tests. - if (firstFrameAfterActivation - && previousTopOfStack != topOfStack - && previousTopOfStack != null - && previousTopOfStack.hasChildIds()) { + if (firstFrameAfterActivation && previousTopOfStack != topOfStack + && previousTopOfStack != null && previousTopOfStack.hasChildIds()) { if (!topOfStack.isSuccessor(previousTopOfStack)) { CallTree commonAncestor = findCommonAncestor(previousTopOfStack, topOfStack); CallTree newParent = commonAncestor != null ? commonAncestor : topOfStack; @@ -798,21 +782,23 @@ private CallTree findCommonAncestor(CallTree previousTopOfStack, CallTree topOfS } /** - * Creates spans for call tree nodes if they are either not a {@linkplain #isPillar() pillar} or - * are a {@linkplain #isLeaf() leaf}. Nodes which are not converted to {@link Span}s are part of - * the {@link Span#stackFrames} for the nodes which do get converted to a span. - * - *

    Parent/child relationships with the regular spans are maintained. One exception is that an - * inferred span can't be the parent of a regular span. That is because the regular spans have - * already been reported once the inferred spans are created. In the future, we might make it - * possible to update the {@link TraceContext#parentId} of a regular span so that it correctly - * reflects being a child of an inferred span. + * Creates spans for call tree nodes if they are either not a {@linkplain #isPillar() pillar} or are a {@linkplain #isLeaf() leaf}. + * Nodes which are not converted to {@link Span}s are part of the {@link Span#stackFrames} for the nodes which do get converted to a span. + *

    + * Parent/child relationships with the regular spans are maintained. + * One exception is that an inferred span can't be the parent of a regular span. + * That is because the regular spans have already been reported once the inferred spans are created. + * In the future, we might make it possible to update the {@link TraceContext#parentId} + * of a regular span so that it correctly reflects being a child of an inferred span. + *

    */ - public int spanify() { + public int spanify(NanoClock clock, Tracer tracer) { + StringBuilder tempBuilder = new StringBuilder(); int createdSpans = 0; List callTrees = getChildren(); for (int i = 0, size = callTrees.size(); i < size; i++) { - createdSpans += callTrees.get(i).spanify(this, rootContext); + createdSpans += callTrees.get(i) + .spanify(this, null, rootContext, clock, tempBuilder, tracer); } return createdSpans; } @@ -821,15 +807,12 @@ public TraceContext getRootContext() { return rootContext; } - public long getEpochMicros(long nanoTime) { - return rootContext.getClock().getEpochMicros(nanoTime); - } /** - * Recycles this tree to the provided pools. First, all child subtrees are recycled recursively - * to the children pool. Then, {@code this} root node is recycled to the root pool. This means - * that the caller of this method should make sure that no reference to this root object is - * held anywhere. + * Recycles this tree to the provided pools. + * First, all child subtrees are recycled recursively to the children pool. + * Then, {@code this} root node is recycled to the root pool. This means that the caller of this method + * should make sure that no reference to this root object is held anywhere. * * @param childrenPool object pool for all non-root nodes * @param rootPool object pool for root nodes diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java new file mode 100644 index 000000000..de1b3178c --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java @@ -0,0 +1,119 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler; + +import co.elastic.apm.otel.profiler.config.WildcardMatcher; +import java.time.Duration; +import java.util.List; + +public class InferredSpansConfiguration { + + private final boolean profilerLoggingEnabled; + private final boolean backupDiagnosticFiles; + private final int asyncProfilerSafeMode; + private final boolean postProcessingEnabled; + private final Duration samplingInterval; + private final Duration inferredSpansMinDuration; + private final List includedClasses; + private final List excludedClasses; + private final Duration profilerInterval; + private final Duration profilingDuration; + private final String profilerLibDirectory; + + InferredSpansConfiguration( + boolean profilerLoggingEnabled, + boolean backupDiagnosticFiles, + int asyncProfilerSafeMode, + boolean postProcessingEnabled, + Duration samplingInterval, + Duration inferredSpansMinDuration, + List includedClasses, + List excludedClasses, + Duration profilerInterval, + Duration profilingDuration, + String profilerLibDirectory + ) { + this.profilerLoggingEnabled = profilerLoggingEnabled; + this.backupDiagnosticFiles = backupDiagnosticFiles; + this.asyncProfilerSafeMode = asyncProfilerSafeMode; + this.postProcessingEnabled = postProcessingEnabled; + this.samplingInterval = samplingInterval; + this.inferredSpansMinDuration = inferredSpansMinDuration; + this.includedClasses = includedClasses; + this.excludedClasses = excludedClasses; + this.profilerInterval = profilerInterval; + this.profilingDuration = profilingDuration; + this.profilerLibDirectory = profilerLibDirectory; + } + + public static InferredSpansProcessorBuilder builder() { + return new InferredSpansProcessorBuilder(); + } + + public boolean isProfilingLoggingEnabled() { + return profilerLoggingEnabled; + } + + public int getAsyncProfilerSafeMode() { + return asyncProfilerSafeMode; + } + + public Duration getSamplingInterval() { + return samplingInterval; + } + + public Duration getInferredSpansMinDuration() { + return inferredSpansMinDuration; + } + + public List getIncludedClasses() { + return includedClasses; + } + + public List getExcludedClasses() { + return excludedClasses; + } + + public Duration getProfilingInterval() { + return profilerInterval; + } + + public Duration getProfilingDuration() { + return profilingDuration; + } + + public boolean isNonStopProfiling() { + return getProfilingDuration().toMillis() >= getProfilingInterval().toMillis(); + } + + public boolean isBackupDiagnosticFiles() { + return backupDiagnosticFiles; + } + + public String getProfilerLibDirectory() { + return profilerLibDirectory == null || profilerLibDirectory.isEmpty() ? System.getProperty( + "java.io.tmpdir") + : profilerLibDirectory; + } + + public boolean isPostProcessingEnabled() { + return postProcessingEnabled; + } + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java new file mode 100644 index 000000000..011911712 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java @@ -0,0 +1,98 @@ +package co.elastic.apm.otel.profiler; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import java.io.File; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +public class InferredSpansProcessor implements SpanProcessor { + + private static final Logger logger = Logger.getLogger(InferredSpansProcessor.class.getName()); + + public static final String TRACER_NAME = "elastic-inferred-spans"; + + //Visible for testing + final SamplingProfiler profiler; + + private Tracer tracer; + + InferredSpansProcessor( + InferredSpansConfiguration config, + NanoClock clock, + boolean startScheduledProfiling, + @Nullable File activationEventsFile, + @Nullable File jfrFile + ) { + profiler = new SamplingProfiler(config, clock, this::getTracer, activationEventsFile, jfrFile); + if (startScheduledProfiling) { + profiler.start(); + } + } + + public static InferredSpansProcessorBuilder builder() { + return new InferredSpansProcessorBuilder(); + } + + /** + * @param provider the provider to use. Null means that {@link GlobalOpenTelemetry} will be used lazily. + */ + public synchronized void setTracerProvider(TracerProvider provider) { + tracer = provider.get(TRACER_NAME); + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + profiler.getClock().onSpanStart(span, parentContext); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + } + + @Override + public boolean isEndRequired() { + return false; + } + + @Override + public CompletableResultCode shutdown() { + CompletableResultCode result = new CompletableResultCode(); + logger.fine("Stopping Inferred Spans Processor"); + Executors.newSingleThreadExecutor().submit(() -> { + try { + profiler.stop(); + result.succeed(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to stop Inferred Spans Processor", e); + result.fail(); + } + }); + return result; + } + + private Tracer getTracer() { + if (tracer == null) { + synchronized (this) { + if (tracer == null) { + setTracerProvider(GlobalOpenTelemetry.get().getTracerProvider()); + } + } + } + return tracer; + } + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java new file mode 100644 index 000000000..34a23fdc8 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java @@ -0,0 +1,195 @@ +package co.elastic.apm.otel.profiler; + +import co.elastic.apm.otel.profiler.config.WildcardMatcher; +import java.io.File; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nullable; + +public class InferredSpansProcessorBuilder { + private boolean profilerLoggingEnabled = true; + private boolean backupDiagnosticFiles = false; + private int asyncProfilerSafeMode = 0; + private boolean postProcessingEnabled = true; + private Duration samplingInterval = Duration.ofMillis(50); + private Duration inferredSpansMinDuration = Duration.ZERO; + private List includedClasses = WildcardMatcher.matchAllList(); + private List excludedClasses = Arrays.asList( + WildcardMatcher.caseSensitiveMatcher("java.*"), + WildcardMatcher.caseSensitiveMatcher("javax.*"), + WildcardMatcher.caseSensitiveMatcher("sun.*"), + WildcardMatcher.caseSensitiveMatcher("com.sun.*"), + WildcardMatcher.caseSensitiveMatcher("jdk.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.tomcat.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.catalina.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.coyote.*"), + WildcardMatcher.caseSensitiveMatcher("org.jboss.as.*"), + WildcardMatcher.caseSensitiveMatcher("org.glassfish.*"), + WildcardMatcher.caseSensitiveMatcher("org.eclipse.jetty.*"), + WildcardMatcher.caseSensitiveMatcher("com.ibm.websphere.*"), + WildcardMatcher.caseSensitiveMatcher("io.undertow.*") + ); + private Duration profilerInterval = Duration.ofSeconds(5); + private Duration profilingDuration = Duration.ofSeconds(5); + private String profilerLibDirectory = null; + + //The following options are only intended to be modified in tests + private NanoClock clock = new SpanAnchoredNanoClock(); + private boolean startScheduledProfiling = true; + private @Nullable File activationEventsFile = null; + private @Nullable File jfrFile = null; + + InferredSpansProcessorBuilder() { + + } + + public InferredSpansProcessor build() { + InferredSpansConfiguration config = new InferredSpansConfiguration( + profilerLoggingEnabled, + backupDiagnosticFiles, + asyncProfilerSafeMode, + postProcessingEnabled, + samplingInterval, + inferredSpansMinDuration, + includedClasses, + excludedClasses, + profilerInterval, + profilingDuration, + profilerLibDirectory + ); + return new InferredSpansProcessor(config, clock, startScheduledProfiling, activationEventsFile, + jfrFile); + } + + /** + * By default, async profiler prints warning messages about missing JVM symbols to standard output. + * Set this option to {@code true} to suppress such messages + */ + public InferredSpansProcessorBuilder profilerLoggingEnabled(boolean profilerLoggingEnabled) { + this.profilerLoggingEnabled = profilerLoggingEnabled; + return this; + } + + public InferredSpansProcessorBuilder backupDiagnosticFiles(boolean backupDiagnosticFiles) { + this.backupDiagnosticFiles = backupDiagnosticFiles; + return this; + } + + /** + * Can be used for analysis: the Async Profiler's area that deals with recovering stack trace frames + * is known to be sensitive in some systems. It is used as a bit mask using values are between 0 and 31, + * where 0 enables all recovery attempts and 31 disables all five (corresponding 1, 2, 4, 8 and 16). + */ + public InferredSpansProcessorBuilder asyncProfilerSafeMode(int asyncProfilerSafeMode) { + this.asyncProfilerSafeMode = asyncProfilerSafeMode; + return this; + } + + /** + * Can be used to test the effect of the async-profiler in isolation from the agent's post-processing. + */ + public InferredSpansProcessorBuilder postProcessingEnabled(boolean postProcessingEnabled) { + this.postProcessingEnabled = postProcessingEnabled; + return this; + } + + /** + * The frequency at which stack traces are gathered within a profiling session. + * The lower you set it, the more accurate the durations will be. + * This comes at the expense of higher overhead and more spans for potentially irrelevant operations. + * The minimal duration of a profiling-inferred span is the same as the value of this setting. + */ + public InferredSpansProcessorBuilder samplingInterval(Duration samplingInterval) { + this.samplingInterval = samplingInterval; + return this; + } + + /** + * The minimum duration of an inferred span. + * Note that the min duration is also implicitly set by the sampling interval. + * However, increasing the sampling interval also decreases the accuracy of the duration of inferred spans. + */ + public InferredSpansProcessorBuilder inferredSpansMinDuration(Duration inferredSpansMinDuration) { + this.inferredSpansMinDuration = inferredSpansMinDuration; + return this; + } + + /** + * If set, the agent will only create inferred spans for methods which match this list. + * Setting a value may slightly reduce overhead and can reduce clutter by only creating spans for the classes you are interested in. + * Example: org.example.myapp.* + */ + public InferredSpansProcessorBuilder includedClasses(List includedClasses) { + this.includedClasses = includedClasses; + return this; + } + + /** + * Excludes classes for which no profiler-inferred spans should be created. + */ + public InferredSpansProcessorBuilder excludedClasses(List excludedClasses) { + this.excludedClasses = excludedClasses; + return this; + } + + /** + * The interval at which profiling sessions should be started. + */ + public InferredSpansProcessorBuilder profilerInterval(Duration profilerInterval) { + this.profilerInterval = profilerInterval; + return this; + } + + /** + * The duration of a profiling session. + * For sampled transactions which fall within a profiling session (they start after and end before the session), + * so-called inferred spans will be created. + * They appear in the trace waterfall view like regular spans. + * NOTE: It is not recommended to set much higher durations as it may fill the activation events file and async-profiler's frame buffer. + * Warnings will be logged if the activation events file is full. + * If you want to have more profiling coverage, try decreasing {@link #profilerInterval(Duration)}. + */ + public InferredSpansProcessorBuilder profilingDuration(Duration profilingDuration) { + this.profilingDuration = profilingDuration; + return this; + } + + public InferredSpansProcessorBuilder profilerLibDirectory(String profilerLibDirectory) { + this.profilerLibDirectory = profilerLibDirectory; + return this; + } + + /** + * For testing only. + */ + InferredSpansProcessorBuilder clock(NanoClock clock) { + this.clock = clock; + return this; + } + + /** + * For testing only. + */ + InferredSpansProcessorBuilder startScheduledProfiling(boolean startScheduledProfiling) { + this.startScheduledProfiling = startScheduledProfiling; + return this; + } + + /** + * For testing only. + */ + InferredSpansProcessorBuilder activationEventsFile(@Nullable File activationEventsFile) { + this.activationEventsFile = activationEventsFile; + return this; + } + + /** + * For testing only. + */ + InferredSpansProcessorBuilder jfrFile(@Nullable File jfrFile) { + this.jfrFile = jfrFile; + return this; + } + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java index 02bb3aa8b..f8d3c552f 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java @@ -16,9 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; public interface NanoClock { + void onSpanStart(ReadWriteSpan started, Context parentContext); + long nanoTime(); + + long getAnchor(Span parent); + + long toEpochNanos(long anchor, long recordedNanoTime); + + void periodicCleanup(); } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java index e7551c52e..a80303740 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java @@ -16,43 +16,128 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; -import co.elastic.apm.agent.impl.ActivationListener; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.impl.transaction.AbstractSpan; -import co.elastic.apm.agent.sdk.internal.ThreadUtil; -import java.util.Objects; +import co.elastic.apm.otel.profiler.util.ThreadUtils; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextStorage; +import io.opentelemetry.context.Scope; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; -public class ProfilingActivationListener implements ActivationListener { +public class ProfilingActivationListener implements Closeable { - private final ElasticApmTracer tracer; - private final SamplingProfiler profiler; + static { + ContextStorage.addWrapper(ContextStorageWrapper::new); + } - public ProfilingActivationListener(ElasticApmTracer tracer) { - this(tracer, Objects.requireNonNull(tracer.getLifecycleListener(SamplingProfiler.class))); + public static void ensureInitialized() { + //does nothing but ensures that the static initializer ran } - ProfilingActivationListener(ElasticApmTracer tracer, SamplingProfiler profiler) { - this.tracer = tracer; + private static volatile List activeListeners = Collections.emptyList(); + + + private static class ContextStorageWrapper implements ContextStorage { + + private final ContextStorage delegate; + + private ContextStorageWrapper(ContextStorage delegate) { + this.delegate = delegate; + } + + @Override + public Scope attach(Context toAttach) { + List listeners = activeListeners; + if (listeners.isEmpty()) { + // no unnecessary allocations when no listener is active + return delegate.attach(toAttach); + } + Span attached = spanFromContextNullSafe(toAttach); + Span oldCtx = spanFromContextNullSafe(delegate.current()); + for (ProfilingActivationListener listener : listeners) { + listener.beforeActivate(oldCtx, attached); + } + Scope delegateScope = delegate.attach(toAttach); + return () -> { + delegateScope.close(); + Span newCtx = spanFromContextNullSafe(delegate.current()); + for (ProfilingActivationListener listener : listeners) { + listener.afterDeactivate(attached, newCtx); + } + }; + } + + Span spanFromContextNullSafe(@Nullable Context context) { + if (context == null) { + return Span.getInvalid(); + } + return Span.fromContext(context); + } + + @Nullable + @Override + public Context current() { + return delegate.current(); + } + + @Override + public Context root() { + return delegate.root(); + } + } + + private final SamplingProfiler profiler; + + private ProfilingActivationListener(SamplingProfiler profiler) { this.profiler = profiler; } + public static ProfilingActivationListener register(SamplingProfiler profiler) { + ProfilingActivationListener result = new ProfilingActivationListener(profiler); + synchronized (ProfilingActivationListener.class) { + List listenersList = new ArrayList<>(activeListeners); + listenersList.add(result); + activeListeners = Collections.unmodifiableList(listenersList); + } + return result; + } + @Override - public void beforeActivate(AbstractSpan context) { - if (context.isSampled() && !ThreadUtil.isVirtual(Thread.currentThread())) { - AbstractSpan active = tracer.getActive(); + public void close() { + synchronized (ProfilingActivationListener.class) { + List listenersList = new ArrayList<>(activeListeners); + listenersList.remove(this); + activeListeners = Collections.unmodifiableList(listenersList); + } + } + + public void beforeActivate(Span oldContext, Span newContext) { + if (newContext.getSpanContext().isValid() + && newContext.getSpanContext().isSampled() + && !ThreadUtils.isVirtual(Thread.currentThread()) + ) { profiler.onActivation( - context.getTraceContext(), active != null ? active.getTraceContext() : null); + newContext, + oldContext.getSpanContext().isValid() ? oldContext : null + ); } } - @Override - public void afterDeactivate(AbstractSpan deactivatedContext) { - if (deactivatedContext.isSampled() && !ThreadUtil.isVirtual(Thread.currentThread())) { - AbstractSpan active = tracer.getActive(); + public void afterDeactivate(Span deactivatedContext, Span newContext) { + if (deactivatedContext.getSpanContext().isValid() + && deactivatedContext.getSpanContext().isSampled() + && !ThreadUtils.isVirtual(Thread.currentThread()) + ) { profiler.onDeactivation( - deactivatedContext.getTraceContext(), active != null ? active.getTraceContext() : null); + deactivatedContext, + newContext.getSpanContext().isValid() ? newContext : null + ); } } + } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingConfiguration.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingConfiguration.java deleted file mode 100644 index 15eaf90a9..000000000 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingConfiguration.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; - -import static co.elastic.apm.agent.tracer.configuration.RangeValidator.isInRange; -import static co.elastic.apm.agent.tracer.configuration.RangeValidator.min; - -import co.elastic.apm.agent.common.util.WildcardMatcher; -import co.elastic.apm.agent.tracer.configuration.ListValueConverter; -import co.elastic.apm.agent.tracer.configuration.TimeDuration; -import co.elastic.apm.agent.tracer.configuration.TimeDurationValueConverter; -import co.elastic.apm.agent.tracer.configuration.WildcardMatcherValueConverter; -import java.util.Arrays; -import java.util.List; -import org.stagemonitor.configuration.ConfigurationOption; -import org.stagemonitor.configuration.ConfigurationOptionProvider; - -public class ProfilingConfiguration extends ConfigurationOptionProvider { - - private static final String PROFILING_CATEGORY = "Profiling"; - - private final ConfigurationOption profilingEnabled = - ConfigurationOption.booleanOption() - .key("profiling_inferred_spans_enabled") - .configurationCategory(PROFILING_CATEGORY) - .description( - "Set to `true` to make the agent create spans for method executions based on\n" - + "https://github.com/jvm-profiling-tools/async-profiler[async-profiler], a sampling aka statistical profiler.\n" - + "\n" - + "Due to the nature of how sampling profilers work,\n" - + "the duration of the inferred spans are not exact, but only estimations.\n" - + "The <> lets you fine tune the trade-off between accuracy and overhead.\n" - + "\n" - + "The inferred spans are created after a profiling session has ended.\n" - + "This means there is a delay between the regular and the inferred spans being visible in the UI.\n" - + "\n" - + "Only platform threads are supported. Virtual threads are not supported and will not be profiled.\n" - + "\n" - + "NOTE: This feature is not available on Windows and on OpenJ9") - .dynamic(true) - .tags("added[1.15.0]", "experimental") - .buildWithDefault(false); - - private final ConfigurationOption profilerLoggingEnabled = - ConfigurationOption.booleanOption() - .key("profiling_inferred_spans_logging_enabled") - .configurationCategory(PROFILING_CATEGORY) - .description( - "By default, async profiler prints warning messages about missing JVM symbols to standard output. \n" - + "Set this option to `true` to suppress such messages") - .dynamic(true) - .tags("added[1.37.0]") - .buildWithDefault(true); - - private final ConfigurationOption backupDiagnosticFiles = - ConfigurationOption.booleanOption() - .key("profiling_inferred_spans_backup_diagnostic_files") - .configurationCategory(PROFILING_CATEGORY) - .dynamic(true) - .tags("added[1.15.0]", "internal") - .buildWithDefault(false); - - private final ConfigurationOption asyncProfilerSafeMode = - ConfigurationOption.integerOption() - .key("async_profiler_safe_mode") - .configurationCategory(PROFILING_CATEGORY) - .dynamic(false) - .description( - "Can be used for analysis: the Async Profiler's area that deals with recovering stack trace frames \n" - + "is known to be sensitive in some systems. It is used as a bit mask using values are between 0 and 31, \n" - + "where 0 enables all recovery attempts and 31 disables all five (corresponding 1, 2, 4, 8 and 16).") - .tags("internal") - .buildWithDefault(0); - - private final ConfigurationOption postProcessingEnabled = - ConfigurationOption.booleanOption() - .key("profiling_inferred_spans_post_processing_enabled") - .configurationCategory(PROFILING_CATEGORY) - .dynamic(true) - .description( - "Can be used to test the effect of the async-profiler in isolation from the agent's post-processing.") - .tags("added[1.18.0]", "internal") - .buildWithDefault(true); - - private final ConfigurationOption samplingInterval = - TimeDurationValueConverter.durationOption("ms") - .key("profiling_inferred_spans_sampling_interval") - .configurationCategory(PROFILING_CATEGORY) - .dynamic(true) - .description( - "The frequency at which stack traces are gathered within a profiling session.\n" - + "The lower you set it, the more accurate the durations will be.\n" - + "This comes at the expense of higher overhead and more spans for potentially irrelevant operations.\n" - + "The minimal duration of a profiling-inferred span is the same as the value of this setting.") - .addValidator(isInRange(TimeDuration.of("1ms"), TimeDuration.of("1s"))) - .tags("added[1.15.0]") - .buildWithDefault(TimeDuration.of("50ms")); - - private final ConfigurationOption inferredSpansMinDuration = - TimeDurationValueConverter.durationOption("ms") - .key("profiling_inferred_spans_min_duration") - .configurationCategory(PROFILING_CATEGORY) - .dynamic(true) - .description( - "The minimum duration of an inferred span.\n" - + "Note that the min duration is also implicitly set by the sampling interval.\n" - + "However, increasing the sampling interval also decreases the accuracy of the duration of inferred spans.") - .tags("added[1.15.0]") - .addValidator(min(TimeDuration.of("0ms"))) - .buildWithDefault(TimeDuration.of("0ms")); - - private final ConfigurationOption> includedClasses = - ConfigurationOption.builder( - new ListValueConverter<>(new WildcardMatcherValueConverter()), List.class) - .key("profiling_inferred_spans_included_classes") - .configurationCategory(PROFILING_CATEGORY) - .description( - "If set, the agent will only create inferred spans for methods which match this list.\n" - + "Setting a value may slightly reduce overhead and can reduce clutter by only creating spans for the classes you are interested in.\n" - + "Example: `org.example.myapp.*`\n" - + "\n" - + WildcardMatcher.DOCUMENTATION) - .dynamic(true) - .tags("added[1.15.0]") - .buildWithDefault(WildcardMatcher.matchAllList()); - - private final ConfigurationOption> excludedClasses = - ConfigurationOption.builder( - new ListValueConverter<>(new WildcardMatcherValueConverter()), List.class) - .key("profiling_inferred_spans_excluded_classes") - .configurationCategory(PROFILING_CATEGORY) - .description( - "Excludes classes for which no profiler-inferred spans should be created.\n" - + "\n" - + WildcardMatcher.DOCUMENTATION) - .dynamic(true) - .tags("added[1.15.0]") - .buildWithDefault( - Arrays.asList( - WildcardMatcher.caseSensitiveMatcher("java.*"), - WildcardMatcher.caseSensitiveMatcher("javax.*"), - WildcardMatcher.caseSensitiveMatcher("sun.*"), - WildcardMatcher.caseSensitiveMatcher("com.sun.*"), - WildcardMatcher.caseSensitiveMatcher("jdk.*"), - WildcardMatcher.caseSensitiveMatcher("org.apache.tomcat.*"), - WildcardMatcher.caseSensitiveMatcher("org.apache.catalina.*"), - WildcardMatcher.caseSensitiveMatcher("org.apache.coyote.*"), - WildcardMatcher.caseSensitiveMatcher("org.jboss.as.*"), - WildcardMatcher.caseSensitiveMatcher("org.glassfish.*"), - WildcardMatcher.caseSensitiveMatcher("org.eclipse.jetty.*"), - WildcardMatcher.caseSensitiveMatcher("com.ibm.websphere.*"), - WildcardMatcher.caseSensitiveMatcher("io.undertow.*"))); - - private final ConfigurationOption profilerInterval = - TimeDurationValueConverter.durationOption("s") - .key("profiling_inferred_spans_interval") - .description("The interval at which profiling sessions should be started.") - .configurationCategory(PROFILING_CATEGORY) - .addValidator(min(TimeDuration.of("0ms"))) - .dynamic(true) - .tags("added[1.15.0]", "internal") - .buildWithDefault(TimeDuration.of("5s")); - - private final ConfigurationOption profilingDuration = - TimeDurationValueConverter.durationOption("s") - .key("profiling_inferred_spans_duration") - .description( - "The duration of a profiling session.\n" - + "For sampled transactions which fall within a profiling session (they start after and end before the session),\n" - + "so-called inferred spans will be created.\n" - + "They appear in the trace waterfall view like regular spans.\n" - + "\n" - + "NOTE: It is not recommended to set much higher durations as it may fill the activation events file and async-profiler's frame buffer.\n" - + "Warnings will be logged if the activation events file is full.\n" - + "If you want to have more profiling coverage, try decreasing <>.") - .configurationCategory(PROFILING_CATEGORY) - .dynamic(true) - .addValidator(isInRange(TimeDuration.of("1s"), TimeDuration.of("30s"))) - .tags("added[1.15.0]", "internal") - .buildWithDefault(TimeDuration.of("5s")); - - private final ConfigurationOption profilerLibDirectory = - ConfigurationOption.stringOption() - .key("profiling_inferred_spans_lib_directory") - .description( - "Profiling requires that the https://github.com/jvm-profiling-tools/async-profiler[async-profiler] shared library " - + "is exported to a temporary location and loaded by the JVM.\n" - + "The partition backing this location must be executable, however in some server-hardened environments, " - + "`noexec` may be set on the standard `/tmp` partition, leading to `java.lang.UnsatisfiedLinkError` errors.\n" - + "Set this property to an alternative directory (e.g. `/var/tmp`) to resolve this.\n" - + "If unset, the value of the `java.io.tmpdir` system property will be used.") - .configurationCategory(PROFILING_CATEGORY) - .dynamic(false) - .tags("added[1.18.0]") - .build(); - - public boolean isProfilingEnabled() { - return profilingEnabled.get(); - } - - public boolean isProfilingLoggingEnabled() { - return profilerLoggingEnabled.get(); - } - - public int getAsyncProfilerSafeMode() { - return asyncProfilerSafeMode.get(); - } - - public TimeDuration getSamplingInterval() { - return samplingInterval.get(); - } - - public TimeDuration getInferredSpansMinDuration() { - return inferredSpansMinDuration.get(); - } - - public List getIncludedClasses() { - return includedClasses.get(); - } - - public List getExcludedClasses() { - return excludedClasses.get(); - } - - public TimeDuration getProfilingInterval() { - return profilerInterval.get(); - } - - public TimeDuration getProfilingDuration() { - return profilingDuration.get(); - } - - public boolean isNonStopProfiling() { - return getProfilingDuration().getMillis() >= getProfilingInterval().getMillis(); - } - - public boolean isBackupDiagnosticFiles() { - return backupDiagnosticFiles.get(); - } - - public String getProfilerLibDirectory() { - return profilerLibDirectory.isDefault() - ? System.getProperty("java.io.tmpdir") - : profilerLibDirectory.get(); - } - - public boolean isPostProcessingEnabled() { - return postProcessingEnabled.get(); - } -} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingFactory.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingFactory.java deleted file mode 100644 index 4c2d58a8e..000000000 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; - -import co.elastic.apm.agent.context.AbstractLifecycleListener; -import co.elastic.apm.agent.impl.ElasticApmTracer; - -public class ProfilingFactory extends AbstractLifecycleListener { - - private final SamplingProfiler profiler; - private final NanoClock nanoClock; - - public ProfilingFactory(ElasticApmTracer tracer) { - boolean envTest = false; - // in unit tests, where assertions are enabled, this envTest is true - assert envTest = true; - nanoClock = envTest ? new FixedNanoClock() : new SystemNanoClock(); - profiler = new SamplingProfiler(tracer, nanoClock); - } - - @Override - public void start(ElasticApmTracer tracer) { - profiler.start(tracer); - tracer.registerSpanListener(new ProfilingActivationListener(tracer, profiler)); - } - - @Override - public void stop() throws Exception { - profiler.stop(); - } - - public SamplingProfiler getProfiler() { - return profiler; - } - - public NanoClock getNanoClock() { - return nanoClock; - } -} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java index 54c96d377..e95c32d1d 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java @@ -16,27 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; import static java.nio.file.StandardOpenOption.READ; import static java.nio.file.StandardOpenOption.WRITE; -import co.elastic.apm.agent.common.util.WildcardMatcher; -import co.elastic.apm.agent.context.AbstractLifecycleListener; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.impl.transaction.Span; -import co.elastic.apm.agent.impl.transaction.StackFrame; -import co.elastic.apm.agent.impl.transaction.TraceContext; -import co.elastic.apm.agent.profiler.asyncprofiler.AsyncProfiler; -import co.elastic.apm.agent.profiler.asyncprofiler.JfrParser; -import co.elastic.apm.agent.profiler.collections.Long2ObjectHashMap; -import co.elastic.apm.agent.sdk.internal.util.ExecutorUtils; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; -import co.elastic.apm.agent.tracer.configuration.CoreConfiguration; -import co.elastic.apm.agent.tracer.configuration.TimeDuration; -import co.elastic.apm.agent.tracer.pooling.Allocator; -import co.elastic.apm.agent.tracer.pooling.ObjectPool; +import co.elastic.apm.otel.profiler.asyncprofiler.AsyncProfiler; +import co.elastic.apm.otel.profiler.asyncprofiler.JfrParser; +import co.elastic.apm.otel.profiler.collections.Long2ObjectHashMap; +import co.elastic.apm.otel.profiler.config.WildcardMatcher; +import co.elastic.apm.otel.profiler.pooling.Allocator; +import co.elastic.apm.otel.profiler.pooling.ObjectPool; import com.lmax.disruptor.EventFactory; import com.lmax.disruptor.EventPoller; import com.lmax.disruptor.EventTranslatorTwoArg; @@ -44,6 +34,8 @@ import com.lmax.disruptor.Sequence; import com.lmax.disruptor.SequenceBarrier; import com.lmax.disruptor.WaitStrategy; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; import java.io.File; import java.io.IOException; import java.nio.Buffer; @@ -54,69 +46,73 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nullable; /** - * Correlates {@link ActivationEvent}s with {@link StackFrame}s which are recorded by {@link - * AsyncProfiler}, a native
    {@code - * AsyncGetCallTree}-based (and therefore non - * safepoint-biased) JVMTI agent. - * - *

    Recording of {@link ActivationEvent}s: - * - *

    The {@link #onActivation} and {@link #onDeactivation} methods are called by {@link - * ProfilingActivationListener} which register an {@link ActivationEvent} to a {@linkplain - * #eventBuffer ring buffer} whenever a {@link Span} gets {@link Span#activate()}d or {@link - * Span#deactivate()}d while a {@linkplain #profilingSessionOngoing profiling session is ongoing}. A - * background thread consumes the {@link ActivationEvent}s and writes them to a {@linkplain - * #activationEventsBuffer direct buffer} which is flushed to a {@linkplain - * #activationEventsFileChannel file}. That is necessary because within a profiling session (which - * lasts 10s by default) there may be many more {@link ActivationEvent}s than the ring buffer {@link - * #RING_BUFFER_SIZE can hold}. The file can hold {@link #ACTIVATION_EVENTS_IN_FILE} events and each - * is {@link ActivationEvent#SERIALIZED_SIZE} in size. This process is completely garbage free - * thanks to the {@link RingBuffer} acting as an object pool for {@link ActivationEvent}s. - * - *

    Recording stack traces: - * - *

    The same background thread that processes the {@link ActivationEvent}s starts the wall clock - * profiler of async-profiler via {@link AsyncProfiler#execute(String)}. After the {@link - * ProfilingConfiguration#getProfilingDuration()} is over it stops the profiling and starts - * processing the JFR file created by async-profiler with {@link JfrParser}. - * - *

    Correlating {@link ActivationEvent}s with the traces recorded by {@link AsyncProfiler}: - * - *

    After both the JFR file and the file containing the {@link ActivationEvent}s have been - * written, it's now time to process them in tandem by correlating based on thread ids and - * timestamps. The result of this correlation, performed by {@link #processTraces}, are {@link - * CallTree}s which are created for each thread which has seen an {@linkplain Span#activate() - * activation} and at least one stack trace. Once {@linkplain - * ActivationEvent#handleDeactivationEvent(SamplingProfiler) handling the deactivation event} of the - * root span in a thread (after which {@link ElasticApmTracer#getActive()} would return {@code - * null}), the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext) - * converted into regular spans}. - * - *

    Overall, the allocation rate does not depend on the number of {@link ActivationEvent}s but - * only on {@link ProfilingConfiguration#getProfilingInterval()} and {@link - * ProfilingConfiguration#getSamplingInterval()}. Having said that, there are some optimizations so - * that the JFR file is not processed at all if there have not been any {@link ActivationEvent} in a - * given profiling session. Also, only if there's a {@link CallTree.Root} for a {@link - * StackTraceEvent}, we will {@link JfrParser#resolveStackTrace(long, boolean, List, int) resolve - * the full stack trace}. + * Correlates {@link ActivationEvent}s with {@link StackFrame}s which are recorded by {@link AsyncProfiler}, + * a native {@code AsyncGetCallTree}-based + * (and therefore non safepoint-biased) + * JVMTI agent. + *

    + * Recording of {@link ActivationEvent}s: + *

    + *

    + * The {@link #onActivation} and {@link #onDeactivation} methods are called by {@link ProfilingActivationListener} + * which register an {@link ActivationEvent} to a {@linkplain #eventBuffer ring buffer} whenever a {@link Span} + * gets {@link Span#activate()}d or {@link Span#deactivate()}d while a {@linkplain #profilingSessionOngoing profiling session is ongoing}. + * A background thread consumes the {@link ActivationEvent}s and writes them to a {@linkplain #activationEventsBuffer direct buffer} + * which is flushed to a {@linkplain #activationEventsFileChannel file}. + * That is necessary because within a profiling session (which lasts 10s by default) there may be many more {@link ActivationEvent}s + * than the ring buffer {@link #RING_BUFFER_SIZE can hold}. + * The file can hold {@link #ACTIVATION_EVENTS_IN_FILE} events and each is {@link ActivationEvent#SERIALIZED_SIZE} in size. + * This process is completely garbage free thanks to the {@link RingBuffer} acting as an object pool for {@link ActivationEvent}s. + *

    + *

    + * Recording stack traces: + *

    + *

    + * The same background thread that processes the {@link ActivationEvent}s starts the wall clock profiler of async-profiler via + * {@link AsyncProfiler#execute(String)}. + * After the {@link InferredSpansConfiguration#getProfilingDuration()} is over it stops the profiling and starts processing the JFR file created + * by async-profiler with {@link JfrParser}. + *

    + *

    + * Correlating {@link ActivationEvent}s with the traces recorded by {@link AsyncProfiler}: + *

    + *

    + * After both the JFR file and the file containing the {@link ActivationEvent}s have been written, + * it's now time to process them in tandem by correlating based on thread ids and timestamps. + * The result of this correlation, performed by {@link #processTraces}, + * are {@link CallTree}s which are created for each thread which has seen an {@linkplain Span#activate() activation} + * and at least one stack trace. + * Once {@linkplain ActivationEvent#handleDeactivationEvent(SamplingProfiler) handling the deactivation event} of the root span in a thread + * (after which {@link ElasticApmTracer#getActive()} would return {@code null}), + * the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext) converted into regular spans}. + *

    + *

    + * Overall, the allocation rate does not depend on the number of {@link ActivationEvent}s but only on + * {@link InferredSpansConfiguration#getProfilingInterval()} and {@link InferredSpansConfiguration#getSamplingInterval()}. + * Having said that, there are some optimizations so that the JFR file is not processed at all if there have not been any + * {@link ActivationEvent} in a given profiling session. + * Also, only if there's a {@link CallTree.Root} for a {@link StackTraceEvent}, + * we will {@link JfrParser#resolveStackTrace(long, boolean, List, int) resolve the full stack trace}. + *

    */ -public class SamplingProfiler extends AbstractLifecycleListener implements Runnable { +class SamplingProfiler implements Runnable { - private static final Logger logger = LoggerFactory.getLogger(SamplingProfiler.class); + private static final Logger logger = Logger.getLogger(SamplingProfiler.class.getName()); private static final int ACTIVATION_EVENTS_IN_FILE = 1_000_000; private static final int MAX_STACK_DEPTH = 256; private static final int PRE_ALLOCATE_ACTIVATION_EVENTS_FILE_MB = 10; @@ -124,138 +120,114 @@ public class SamplingProfiler extends AbstractLifecycleListener implements Runna ACTIVATION_EVENTS_IN_FILE * ActivationEvent.SERIALIZED_SIZE; private static final int ACTIVATION_EVENTS_BUFFER_SIZE = ActivationEvent.SERIALIZED_SIZE * 4 * 1024; - private final EventTranslatorTwoArg - ACTIVATION_EVENT_TRANSLATOR = - new EventTranslatorTwoArg() { - @Override - public void translateTo( - ActivationEvent event, - long sequence, - TraceContext active, - TraceContext previouslyActive) { - event.activation( - active, Thread.currentThread().getId(), previouslyActive, nanoClock.nanoTime()); - } - }; - private final EventTranslatorTwoArg - DEACTIVATION_EVENT_TRANSLATOR = - new EventTranslatorTwoArg() { - @Override - public void translateTo( - ActivationEvent event, - long sequence, - TraceContext active, - TraceContext previouslyActive) { - event.deactivation( - active, Thread.currentThread().getId(), previouslyActive, nanoClock.nanoTime()); - } - }; + private final EventTranslatorTwoArg ACTIVATION_EVENT_TRANSLATOR = + new EventTranslatorTwoArg() { + @Override + public void translateTo(ActivationEvent event, long sequence, Span active, + Span previouslyActive) { + event.activation(active, Thread.currentThread().getId(), previouslyActive, + nanoClock.nanoTime(), nanoClock); + } + }; + private final EventTranslatorTwoArg DEACTIVATION_EVENT_TRANSLATOR = + new EventTranslatorTwoArg() { + @Override + public void translateTo(ActivationEvent event, long sequence, Span active, + Span previouslyActive) { + event.deactivation(active, Thread.currentThread().getId(), previouslyActive, + nanoClock.nanoTime(), nanoClock); + } + }; // sizeof(ActivationEvent) is 176B so the ring buffer should be around 880KiB static final int RING_BUFFER_SIZE = 4 * 1024; - private final ProfilingConfiguration config; - private final CoreConfiguration coreConfig; + //Visible for testing + final InferredSpansConfiguration config; private final ScheduledExecutorService scheduler; private final Long2ObjectHashMap profiledThreads = new Long2ObjectHashMap<>(); private final RingBuffer eventBuffer; private volatile boolean profilingSessionOngoing = false; private final Sequence sequence; - private final ElasticApmTracer tracer; private final NanoClock nanoClock; private final ObjectPool rootPool; private final ThreadMatcher threadMatcher = new ThreadMatcher(); private final EventPoller poller; - @Nullable private File jfrFile; + @Nullable + private File jfrFile; private boolean canDeleteJfrFile; - private final WriteActivationEventToFileHandler writeActivationEventToFileHandler = - new WriteActivationEventToFileHandler(); - @Nullable private JfrParser jfrParser; + private final WriteActivationEventToFileHandler writeActivationEventToFileHandler = new WriteActivationEventToFileHandler(); + @Nullable + private JfrParser jfrParser; private volatile int profilingSessions; private final ByteBuffer activationEventsBuffer; - /** - * Used to efficiently write {@link #activationEventsBuffer} via {@link - * FileChannel#write(ByteBuffer)} + * Used to efficiently write {@link #activationEventsBuffer} via {@link FileChannel#write(ByteBuffer)} */ - @Nullable private File activationEventsFile; - + @Nullable + private File activationEventsFile; private boolean canDeleteActivationEventsFile; - @Nullable private FileChannel activationEventsFileChannel; + @Nullable + private FileChannel activationEventsFileChannel; private final ObjectPool callTreePool; private final TraceContext contextForLogging; + private final ProfilingActivationListener activationListener; + private boolean previouslyEnabled = false; - /** - * Creates a sampling profiler using temporary files - * - * @param tracer tracer - * @param nanoClock clock - */ - public SamplingProfiler(ElasticApmTracer tracer, NanoClock nanoClock) { - this(tracer, nanoClock, null, null); - } + private final Supplier tracerProvider; /** * Creates a sampling profiler, optionally relying on existing files. - * - *

    This constructor is most likely used for tests that rely on a known set of files + *

    + * This constructor is most likely used for tests that rely on a known set of files * * @param tracer tracer * @param nanoClock clock * @param activationEventsFile activation events file, if {@literal null} a temp file will be used * @param jfrFile java flight recorder file, if {@literal null} a temp file will be used instead */ - public SamplingProfiler( - final ElasticApmTracer tracer, - NanoClock nanoClock, - @Nullable File activationEventsFile, - @Nullable File jfrFile) { - this.tracer = tracer; - this.config = tracer.getConfig(ProfilingConfiguration.class); - this.coreConfig = tracer.getConfig(CoreConfiguration.class); - this.scheduler = ExecutorUtils.createSingleThreadSchedulingDaemonPool("sampling-profiler"); + SamplingProfiler(InferredSpansConfiguration config, NanoClock nanoClock, + Supplier tracerProvider, + @Nullable File activationEventsFile, @Nullable File jfrFile) { + this.config = config; + this.tracerProvider = tracerProvider; + this.scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> { + Thread result = new Thread(runnable); + result.setName("elastic-otel-inferred-spans"); + return result; + }); this.nanoClock = nanoClock; this.eventBuffer = createRingBuffer(); this.sequence = new Sequence(); // tells the ring buffer to not override slots which have not been read yet this.eventBuffer.addGatingSequences(sequence); this.poller = eventBuffer.newPoller(); - contextForLogging = TraceContext.with64BitId(tracer); - this.callTreePool = - tracer - .getObjectPoolFactory() - .createRecyclableObjectPool( - 2 * 1024, - new Allocator() { - @Override - public CallTree createInstance() { - return new CallTree(); - } - }); - // call tree roots are pooled so that fast activations/deactivations with no associated stack - // traces don't cause allocations - this.rootPool = - tracer - .getObjectPoolFactory() - .createRecyclableObjectPool( - 512, - new Allocator() { - @Override - public CallTree.Root createInstance() { - return new CallTree.Root(tracer); - } - }); + contextForLogging = new TraceContext(); + this.callTreePool = ObjectPool.createRecyclable(2 * 1024, new Allocator() { + @Override + public CallTree createInstance() { + return new CallTree(); + } + }); + // call tree roots are pooled so that fast activations/deactivations with no associated stack traces don't cause allocations + this.rootPool = ObjectPool.createRecyclable(512, new Allocator() { + @Override + public CallTree.Root createInstance() { + return new CallTree.Root(); + } + }); this.jfrFile = jfrFile; activationEventsBuffer = ByteBuffer.allocateDirect(ACTIVATION_EVENTS_BUFFER_SIZE); this.activationEventsFile = activationEventsFile; + activationListener = ProfilingActivationListener.register(this); } /** - * For testing only! This method must only be called in tests and some period after activation / - * deactivation events, as otherwise it is racy. + * For testing only! + * This method must only be called in tests and some period after activation / deactivation events, as otherwise it is racy. * * @param thread the Thread to check. * @return true, if profiling is active for the given thread. @@ -276,9 +248,8 @@ private synchronized void createFilesIfRequired() throws IOException { canDeleteActivationEventsFile = true; } if (activationEventsFileChannel == null || !activationEventsFileChannel.isOpen()) { - activationEventsFileChannel = - FileChannel.open( - activationEventsFile.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE); + activationEventsFileChannel = FileChannel.open(activationEventsFile.toPath(), + StandardOpenOption.READ, StandardOpenOption.WRITE); } if (activationEventsFileChannel.size() == 0) { preAllocate(activationEventsFileChannel, PRE_ALLOCATE_ACTIVATION_EVENTS_FILE_MB); @@ -317,26 +288,25 @@ public ActivationEvent newInstance() { /** * Called whenever a span is activated. - * - *

    This and {@link #onDeactivation} are the only methods which are executed in a multi-threaded + *

    + * This and {@link #onDeactivation} are the only methods which are executed in a multi-threaded * context. + *

    * * @param activeSpan the span which is about to be activated * @param previouslyActive the span which has previously been activated - * @return {@code true}, if the event could be processed, {@code false} if the internal event - * queue is full which means the event has been discarded + * @return {@code true}, if the event could be processed, {@code false} if the internal event queue is full which means the event has been discarded */ - public boolean onActivation(TraceContext activeSpan, @Nullable TraceContext previouslyActive) { + public boolean onActivation(Span activeSpan, @Nullable Span previouslyActive) { if (profilingSessionOngoing) { if (previouslyActive == null) { - AsyncProfiler.getInstance( - config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()) - .enableProfilingCurrentThread(); + AsyncProfiler.getInstance(config.getProfilerLibDirectory(), + config.getAsyncProfilerSafeMode()).enableProfilingCurrentThread(); } - boolean success = - eventBuffer.tryPublishEvent(ACTIVATION_EVENT_TRANSLATOR, activeSpan, previouslyActive); - if (!success && logger.isDebugEnabled()) { - logger.debug("Could not add activation event to ring buffer as no slots are available"); + boolean success = eventBuffer.tryPublishEvent(ACTIVATION_EVENT_TRANSLATOR, activeSpan, + previouslyActive); + if (!success) { + logger.fine("Could not add activation event to ring buffer as no slots are available"); } return success; } @@ -345,26 +315,25 @@ public boolean onActivation(TraceContext activeSpan, @Nullable TraceContext prev /** * Called whenever a span is deactivated. - * - *

    This and {@link #onActivation} are the only methods which are executed in a multi-threaded + *

    + * This and {@link #onActivation} are the only methods which are executed in a multi-threaded * context. + *

    * * @param activeSpan the span which is about to be activated * @param previouslyActive the span which has previously been activated - * @return {@code true}, if the event could be processed, {@code false} if the internal event - * queue is full which means the event has been discarded + * @return {@code true}, if the event could be processed, {@code false} if the internal event queue is full which means the event has been discarded */ - public boolean onDeactivation(TraceContext activeSpan, @Nullable TraceContext previouslyActive) { + public boolean onDeactivation(Span activeSpan, @Nullable Span previouslyActive) { if (profilingSessionOngoing) { if (previouslyActive == null) { - AsyncProfiler.getInstance( - config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()) - .disableProfilingCurrentThread(); + AsyncProfiler.getInstance(config.getProfilerLibDirectory(), + config.getAsyncProfilerSafeMode()).disableProfilingCurrentThread(); } - boolean success = - eventBuffer.tryPublishEvent(DEACTIVATION_EVENT_TRANSLATOR, activeSpan, previouslyActive); - if (!success && logger.isDebugEnabled()) { - logger.debug("Could not add deactivation event to ring buffer as no slots are available"); + boolean success = eventBuffer.tryPublishEvent(DEACTIVATION_EVENT_TRANSLATOR, activeSpan, + previouslyActive); + if (!success) { + logger.fine("Could not add deactivation event to ring buffer as no slots are available"); } return success; } @@ -374,79 +343,51 @@ public boolean onDeactivation(TraceContext activeSpan, @Nullable TraceContext pr @Override public void run() { - boolean enabled = config.isProfilingEnabled() && tracer.isRunning(); - boolean hasBeenDisabled = previouslyEnabled && !enabled; - previouslyEnabled = enabled; - - if (!enabled) { - if (jfrParser != null) { - jfrParser = null; - } - if (!scheduler.isShutdown()) { - scheduler.schedule(this, config.getProfilingInterval().getMillis(), TimeUnit.MILLISECONDS); - } - - if (hasBeenDisabled) { - // only clear when going from enabled -> disabled state - try { - clear(); - } catch (Throwable throwable) { - logger.error("Error while trying to clear profiler constructs", throwable); - } - } - - return; - } - // lazily create temporary files try { createFilesIfRequired(); } catch (IOException e) { - logger.error("unable to initialize profiling files", e); + logger.log(Level.SEVERE, "unable to initialize profiling files", e); return; } - TimeDuration profilingDuration = config.getProfilingDuration(); + Duration profilingDuration = config.getProfilingDuration(); boolean postProcessingEnabled = config.isPostProcessingEnabled(); setProfilingSessionOngoing(postProcessingEnabled); if (postProcessingEnabled) { - logger.debug("Start full profiling session (async-profiler and agent processing)"); + logger.fine("Start full profiling session (async-profiler and agent processing)"); } else { - logger.debug("Start async-profiler profiling session"); + logger.fine("Start async-profiler profiling session"); } try { profile(profilingDuration); } catch (Throwable t) { setProfilingSessionOngoing(false); - logger.error("Stopping profiler", t); + logger.log(Level.SEVERE, "Stopping profiler", t); return; } - logger.debug("End profiling session"); + logger.fine("End profiling session"); boolean interrupted = Thread.currentThread().isInterrupted(); boolean continueProfilingSession = - config.isNonStopProfiling() - && !interrupted - && config.isProfilingEnabled() - && postProcessingEnabled; + config.isNonStopProfiling() && !interrupted && postProcessingEnabled; setProfilingSessionOngoing(continueProfilingSession); if (!interrupted && !scheduler.isShutdown()) { - long delay = config.getProfilingInterval().getMillis() - profilingDuration.getMillis(); + long delay = config.getProfilingInterval().toMillis() - profilingDuration.toMillis(); scheduler.schedule(this, delay, TimeUnit.MILLISECONDS); } } - private void profile(TimeDuration profilingDuration) throws Exception { - AsyncProfiler asyncProfiler = - AsyncProfiler.getInstance( - config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()); + private void profile(Duration profilingDuration) throws Exception { + AsyncProfiler asyncProfiler = AsyncProfiler.getInstance(config.getProfilerLibDirectory(), + config.getAsyncProfilerSafeMode()); try { String startCommand = createStartCommand(); String startMessage = asyncProfiler.execute(startCommand); - logger.debug(startMessage); + logger.fine(startMessage); if (!profiledThreads.isEmpty()) { restoreFilterState(asyncProfiler); } @@ -454,18 +395,15 @@ private void profile(TimeDuration profilingDuration) throws Exception { //noinspection NonAtomicOperationOnVolatileField profilingSessions++; - // When post-processing is disabled activation events are ignored, but we still need to invoke - // this method - // as it is the one enforcing the sampling session duration. As a side effect it will also - // consume + // When post-processing is disabled activation events are ignored, but we still need to invoke this method + // as it is the one enforcing the sampling session duration. As a side effect it will also consume // residual activation events if post-processing is disabled dynamically consumeActivationEventsFromRingBufferAndWriteToFile(profilingDuration); String stopMessage = asyncProfiler.execute("stop"); - logger.debug(stopMessage); + logger.fine(stopMessage); - // When post-processing is disabled, jfr file will not be parsed and the heavy processing will - // not occur + // When post-processing is disabled, jfr file will not be parsed and the heavy processing will not occur // as this method aborts when no activation events are buffered processTraces(); } catch (InterruptedException | ClosedByInterruptException e) { @@ -478,13 +416,10 @@ private void profile(TimeDuration profilingDuration) throws Exception { } String createStartCommand() { - StringBuilder startCommand = - new StringBuilder("start,jfr,event=wall,cstack=n,interval=") - .append(config.getSamplingInterval().getMillis()) - .append("ms,filter,file=") - .append(jfrFile) - .append(",safemode=") - .append(config.getAsyncProfilerSafeMode()); + StringBuilder startCommand = new StringBuilder("start,jfr,event=wall,cstack=n,interval=") + .append(config.getSamplingInterval().toMillis()).append("ms,filter,file=") + .append(jfrFile) + .append(",safemode=").append(config.getAsyncProfilerSafeMode()); if (!config.isProfilingLoggingEnabled()) { startCommand.append(",log=none"); } @@ -492,8 +427,8 @@ String createStartCommand() { } /** - * When doing continuous profiling (interval=duration), we have to tell async-profiler which - * threads it should profile after re-starting it. + * When doing continuous profiling (interval=duration), + * we have to tell async-profiler which threads it should profile after re-starting it. */ private void restoreFilterState(AsyncProfiler asyncProfiler) { threadMatcher.forEachThread( @@ -510,13 +445,14 @@ public void accept(Thread thread, AsyncProfiler asyncProfiler) { asyncProfiler.enableProfilingThread(thread); } }, - asyncProfiler); + asyncProfiler + ); } - private void consumeActivationEventsFromRingBufferAndWriteToFile(TimeDuration profilingDuration) + private void consumeActivationEventsFromRingBufferAndWriteToFile(Duration profilingDuration) throws Exception { resetActivationEventBuffer(); - long threshold = System.currentTimeMillis() + profilingDuration.getMillis(); + long threshold = System.currentTimeMillis() + profilingDuration.toMillis(); long initialSleep = 100_000; long maxSleep = 10_000_000; long sleep = initialSleep; @@ -533,7 +469,7 @@ private void consumeActivationEventsFromRingBufferAndWriteToFile(TimeDuration pr LockSupport.parkNanos(sleep); } } else { - logger.warn("The activation events file is full. Try lowering the profiling_duration."); + logger.warning("The activation events file is full. Try lowering the profiling_duration."); // the file is full, sleep the rest of the profilingDuration Thread.sleep(Math.max(0, threshold - System.currentTimeMillis())); } @@ -556,7 +492,7 @@ public void processTraces() throws IOException { long eof = startProcessingActivationEventsFile(); if (eof == 0 && activationEventsBuffer.limit() == 0 && profiledThreads.isEmpty()) { - logger.debug("No activation events during this period. Skip processing stack traces."); + logger.fine("No activation events during this period. Skip processing stack traces."); return; } long start = System.nanoTime(); @@ -568,11 +504,10 @@ public void processTraces() throws IOException { try { jfrParser.parse(jfrFile, excludedClasses, includedClasses); final List stackTraceEvents = getSortedStackTraceEvents(jfrParser); - if (logger.isDebugEnabled()) { - logger.debug("Processing {} stack traces", stackTraceEvents.size()); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Processing {0} stack traces", stackTraceEvents.size()); } List stackFrames = new ArrayList<>(); - ElasticApmTracer tracer = this.tracer; ActivationEvent event = new ActivationEvent(); long inferredSpansMinDuration = getInferredSpansMinDurationNs(); for (StackTraceEvent stackTrace : stackTraceEvents) { @@ -581,23 +516,20 @@ public void processTraces() throws IOException { if (root != null) { jfrParser.resolveStackTrace(stackTrace.stackTraceId, true, stackFrames, MAX_STACK_DEPTH); if (stackFrames.size() == MAX_STACK_DEPTH) { - logger.debug( + logger.fine( "Max stack depth reached. Set profiling_included_classes or profiling_excluded_classes."); } // stack frames may not contain any Java frames - // see - // https://github.com/jvm-profiling-tools/async-profiler/issues/271#issuecomment-582430233 + // see https://github.com/jvm-profiling-tools/async-profiler/issues/271#issuecomment-582430233 if (!stackFrames.isEmpty()) { try { - root.addStackTrace( - tracer, stackFrames, stackTrace.nanoTime, callTreePool, inferredSpansMinDuration); + root.addStackTrace(stackFrames, stackTrace.nanoTime, callTreePool, + inferredSpansMinDuration); } catch (Exception e) { - logger.warn( - "Removing call tree for thread {} because of exception while adding a stack trace: {} {}", - stackTrace.threadId, - e.getClass(), - e.getMessage()); - logger.debug(e.getMessage(), e); + logger.log(Level.WARNING, + "Removing call tree for thread {0} because of exception while adding a stack trace: {1} {2}", + new Object[] {stackTrace.threadId, e.getClass(), e.getMessage()}); + logger.log(Level.FINE, e.getMessage(), e); profiledThreads.remove(stackTrace.threadId); } } @@ -608,8 +540,8 @@ public void processTraces() throws IOException { // otherwise we may miss root deactivations processActivationEventsUpTo(System.nanoTime(), event, eof); } finally { - if (logger.isDebugEnabled()) { - logger.debug("Processing traces took {}µs", (System.nanoTime() - start) / 1000); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Processing traces took {0}µs", (System.nanoTime() - start) / 1000); } jfrParser.resetState(); resetActivationEventBuffer(); @@ -621,11 +553,9 @@ private void backupDiagnosticFiles(long eof) throws IOException { Path profilerDir = Paths.get(System.getProperty("java.io.tmpdir"), "profiler"); profilerDir.toFile().mkdir(); - try (FileChannel activationsFile = - FileChannel.open( - profilerDir.resolve(now + "-activations.dat"), - StandardOpenOption.CREATE_NEW, - StandardOpenOption.WRITE)) { + try (FileChannel activationsFile = FileChannel.open( + profilerDir.resolve(now + "-activations.dat"), StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { if (eof > 0) { activationEventsFileChannel.transferTo(0, eof, activationsFile); } else { @@ -638,30 +568,25 @@ private void backupDiagnosticFiles(long eof) throws IOException { } private long getInferredSpansMinDurationNs() { - return Math.max( - config.getInferredSpansMinDuration().getMillis(), - coreConfig.getSpanMinDuration().getMillis()) - * 1_000_000; + return config.getInferredSpansMinDuration().toNanos(); } /** - * Returns stack trace events of relevant threads sorted by timestamp. The events in the JFR file - * are not in order. Even for the same thread, a more recent event might come before an older - * event. In order to be able to correlate stack trace events and activation events, both need to - * be in order. - * - *

    Returns only events for threads where at least one activation happened (because only those - * are profiled by async-profiler) + * Returns stack trace events of relevant threads sorted by timestamp. + * The events in the JFR file are not in order. + * Even for the same thread, a more recent event might come before an older event. + * In order to be able to correlate stack trace events and activation events, both need to be in order. + *

    + * Returns only events for threads where at least one activation happened (because only those are profiled by async-profiler) */ private List getSortedStackTraceEvents(JfrParser jfrParser) throws IOException { final List stackTraceEvents = new ArrayList<>(); - jfrParser.consumeStackTraces( - new JfrParser.StackTraceConsumer() { - @Override - public void onCallTree(long threadId, long stackTraceId, long nanoTime) { - stackTraceEvents.add(new StackTraceEvent(nanoTime, stackTraceId, threadId)); - } - }); + jfrParser.consumeStackTraces(new JfrParser.StackTraceConsumer() { + @Override + public void onCallTree(long threadId, long stackTraceId, long nanoTime) { + stackTraceEvents.add(new StackTraceEvent(nanoTime, stackTraceId, threadId)); + } + }); Collections.sort(stackTraceEvents); return stackTraceEvents; } @@ -680,11 +605,10 @@ public void processActivationEventsUpTo(long timestamp, ActivationEvent event, l readActivationEventsToBuffer(activationEventsFileChannel, eof, buf); } long eventTimestamp = peekLong(buf); - if (eventTimestamp < previousTimestamp && logger.isDebugEnabled()) { - logger.debug( - "Timestamp of current activation event ({}) is lower than the one from the previous event ({})", - eventTimestamp, - previousTimestamp); + if (eventTimestamp < previousTimestamp && logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, + "Timestamp of current activation event ({0}) is lower than the one from the previous event ({1})", + new Object[] {eventTimestamp, previousTimestamp}); } previousTimestamp = eventTimestamp; if (eventTimestamp <= timestamp) { @@ -692,12 +616,10 @@ public void processActivationEventsUpTo(long timestamp, ActivationEvent event, l try { event.handle(this); } catch (Exception e) { - logger.warn( - "Removing call tree for thread {} because of exception while handling activation event: {} {}", - event.threadId, - e.getClass(), - e.getMessage()); - logger.debug(e.getMessage(), e); + logger.log(Level.WARNING, + "Removing call tree for thread {0} because of exception while handling activation event: {1} {2}", + new Object[] {event.threadId, e.getClass(), e.getMessage()}); + logger.log(Level.FINE, e.getMessage(), e); profiledThreads.remove(event.threadId); } } else { @@ -706,8 +628,8 @@ public void processActivationEventsUpTo(long timestamp, ActivationEvent event, l } } - private void readActivationEventsToBuffer( - FileChannel activationEventsFileChannel, long eof, ByteBuffer byteBuffer) throws IOException { + private void readActivationEventsToBuffer(FileChannel activationEventsFileChannel, long eof, + ByteBuffer byteBuffer) throws IOException { Buffer buf = byteBuffer; buf.clear(); long remaining = eof - activationEventsFileChannel.position(); @@ -759,24 +681,28 @@ void copyFromFiles(Path activationEvents, Path traces) throws IOException { createFilesIfRequired(); FileChannel otherActivationsChannel = FileChannel.open(activationEvents, READ); - activationEventsFileChannel.transferFrom( - otherActivationsChannel, 0, otherActivationsChannel.size()); + activationEventsFileChannel.transferFrom(otherActivationsChannel, 0, + otherActivationsChannel.size()); activationEventsFileChannel.position(otherActivationsChannel.size()); FileChannel otherTracesChannel = FileChannel.open(traces, READ); FileChannel.open(jfrFile.toPath(), WRITE) .transferFrom(otherTracesChannel, 0, otherTracesChannel.size()); } - @Override - public void start(ElasticApmTracer tracer) { + + public void start() { + scheduler.scheduleAtFixedRate(nanoClock::periodicCleanup, 500, 500, TimeUnit.MILLISECONDS); scheduler.submit(this); } - @Override + public void stop() throws Exception { // cancels/interrupts the profiling thread // implicitly clears profiled threads - ExecutorUtils.shutdownAndWaitTermination(scheduler); + scheduler.shutdown(); + scheduler.awaitTermination(10, TimeUnit.SECONDS); + + activationListener.close(); if (activationEventsFileChannel != null) { activationEventsFileChannel.close(); @@ -794,8 +720,8 @@ void setProfilingSessionOngoing(boolean profilingSessionOngoing) { this.profilingSessionOngoing = profilingSessionOngoing; if (!profilingSessionOngoing) { clearProfiledThreads(); - } else if (!profiledThreads.isEmpty() && logger.isDebugEnabled()) { - logger.debug("Retaining {} call tree roots", profiledThreads.size()); + } else if (!profiledThreads.isEmpty() && logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Retaining {0} call tree roots", profiledThreads.size()); } } @@ -814,14 +740,13 @@ CallTree.Root getRoot() { void clear() throws IOException { // consume all remaining events from the ring buffer try { - poller.poll( - new EventPoller.Handler() { - @Override - public boolean onEvent(ActivationEvent event, long sequence, boolean endOfBatch) { - SamplingProfiler.this.sequence.set(sequence); - return true; - } - }); + poller.poll(new EventPoller.Handler() { + @Override + public boolean onEvent(ActivationEvent event, long sequence, boolean endOfBatch) { + SamplingProfiler.this.sequence.set(sequence); + return true; + } + }); } catch (Exception e) { throw new RuntimeException(e); } @@ -835,7 +760,9 @@ int getProfilingSessions() { return profilingSessions; } - // -- + public NanoClock getClock() { + return nanoClock; + } public static class StackTraceEvent implements Comparable { private final long nanoTime; @@ -868,66 +795,45 @@ public int compareTo(StackTraceEvent o) { private static class ActivationEvent { public static final int SERIALIZED_SIZE = - Long.SIZE / Byte.SIZE - + // timestamp - Short.SIZE / Byte.SIZE - + // serviceName index - Short.SIZE / Byte.SIZE - + // serviceVersion index - TraceContext.SERIALIZED_LENGTH - + // traceContextBuffer - TraceContext.SERIALIZED_LENGTH - + // previousContextBuffer - 1 - + // rootContext - Long.SIZE / Byte.SIZE - + // threadId + Long.SIZE / Byte.SIZE + // timestamp + TraceContext.SERIALIZED_LENGTH + // traceContextBuffer + TraceContext.SERIALIZED_LENGTH + // previousContextBuffer + 1 + // rootContext + Long.SIZE / Byte.SIZE + // threadId 1; // activation - private static final Map serviceNameMap = new HashMap<>(); - private static final Map serviceNameBackMap = new HashMap<>(); - - private static final Map serviceVersionMap = new HashMap<>(); - private static final Map serviceVersionBackMap = new HashMap<>(); - private long timestamp; - @Nullable private String serviceName; - @Nullable private String serviceVersion; private byte[] traceContextBuffer = new byte[TraceContext.SERIALIZED_LENGTH]; private byte[] previousContextBuffer = new byte[TraceContext.SERIALIZED_LENGTH]; private boolean rootContext; private long threadId; private boolean activation; - public void activation( - TraceContext context, - long threadId, - @Nullable TraceContext previousContext, - long nanoTime) { - set(context, threadId, true, previousContext != null ? previousContext : null, nanoTime); + public void activation(Span context, long threadId, + @Nullable Span previousContext, long nanoTime, NanoClock clock) { + set(context, threadId, true, previousContext, nanoTime, clock); } - public void deactivation( - TraceContext context, - long threadId, - @Nullable TraceContext previousContext, - long nanoTime) { - set(context, threadId, false, previousContext != null ? previousContext : null, nanoTime); + public void deactivation(Span context, long threadId, + @Nullable Span previousContext, long nanoTime, NanoClock clock) { + set(context, threadId, false, previousContext, nanoTime, clock); } - private void set( - TraceContext traceContext, - long threadId, - boolean activation, - @Nullable TraceContext previousContext, - long nanoTime) { - traceContext.serialize(traceContextBuffer); + private void set(Span traceContext, long threadId, boolean activation, + @Nullable Span previousContext, long nanoTime, NanoClock clock) { + TraceContext.serialize( + traceContext.getSpanContext(), + clock.getAnchor(traceContext), + traceContextBuffer + ); this.threadId = threadId; this.activation = activation; - this.serviceName = traceContext.getServiceName(); - this.serviceVersion = traceContext.getServiceVersion(); if (previousContext != null) { - previousContext.serialize(previousContextBuffer); + TraceContext.serialize( + previousContext.getSpanContext(), + clock.getAnchor(previousContext), + previousContextBuffer + ); rootContext = false; } else { rootContext = true; @@ -936,13 +842,10 @@ private void set( } public void handle(SamplingProfiler samplingProfiler) { - if (logger.isDebugEnabled()) { - logger.debug( - "Handling event timestamp={} root={} threadId={} activation={}", - timestamp, - rootContext, - threadId, - activation); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Handling event timestamp={0} root={1} threadId={2} activation={3}", + new Object[] {timestamp, + rootContext, threadId, activation}); } if (activation) { handleActivationEvent(samplingProfiler); @@ -957,45 +860,39 @@ private void handleActivationEvent(SamplingProfiler samplingProfiler) { } else { CallTree.Root root = samplingProfiler.profiledThreads.get(threadId); if (root != null) { - if (logger.isDebugEnabled()) { - logger.debug("Handling activation for thread {}", threadId); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Handling activation for thread {0}", threadId); } root.onActivation(traceContextBuffer, timestamp); - } else if (logger.isDebugEnabled()) { - logger.debug( - "Illegal state when handling activation event for thread {}: no root found for this thread", + } else if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, + "Illegal state when handling activation event for thread {0}: no root found for this thread", threadId); } } } private void startProfiling(SamplingProfiler samplingProfiler) { - CallTree.Root root = - CallTree.createRoot( - samplingProfiler.rootPool, - traceContextBuffer, - serviceName, - serviceVersion, - timestamp); - if (logger.isDebugEnabled()) { - logger.debug( - "Create call tree ({}) for thread {}", - deserialize(samplingProfiler, traceContextBuffer), - threadId); + CallTree.Root root = CallTree.createRoot(samplingProfiler.rootPool, traceContextBuffer, + timestamp); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Create call tree ({0}) for thread {1}", + new Object[] {deserialize(samplingProfiler, traceContextBuffer), threadId}); } CallTree.Root orphaned = samplingProfiler.profiledThreads.put(threadId, root); if (orphaned != null) { - if (logger.isDebugEnabled()) { - logger.warn( - "Illegal state when stopping profiling for thread {}: orphaned root", threadId); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, + "Illegal state when stopping profiling for thread {0}: orphaned root", + threadId); } orphaned.recycle(samplingProfiler.callTreePool, samplingProfiler.rootPool); } } private TraceContext deserialize(SamplingProfiler samplingProfiler, byte[] traceContextBuffer) { - samplingProfiler.contextForLogging.deserialize(traceContextBuffer, null, null); + samplingProfiler.contextForLogging.deserialize(traceContextBuffer); return samplingProfiler.contextForLogging; } @@ -1005,13 +902,13 @@ private void handleDeactivationEvent(SamplingProfiler samplingProfiler) { } else { CallTree.Root root = samplingProfiler.profiledThreads.get(threadId); if (root != null) { - if (logger.isDebugEnabled()) { - logger.debug("Handling deactivation for thread {}", threadId); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Handling deactivation for thread {0}", threadId); } root.onDeactivation(traceContextBuffer, previousContextBuffer, timestamp); - } else if (logger.isDebugEnabled()) { - logger.debug( - "Illegal state when handling deactivation event for thread {}: no root found for this thread", + } else if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, + "Illegal state when handling deactivation event for thread {0}: no root found for this thread", threadId); } } @@ -1020,23 +917,24 @@ private void handleDeactivationEvent(SamplingProfiler samplingProfiler) { private void stopProfiling(SamplingProfiler samplingProfiler) { CallTree.Root callTree = samplingProfiler.profiledThreads.get(threadId); if (callTree != null && callTree.getRootContext().traceIdAndIdEquals(traceContextBuffer)) { - if (logger.isDebugEnabled()) { - logger.debug( - "End call tree ({}) for thread {}", - deserialize(samplingProfiler, traceContextBuffer), - threadId); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "End call tree ({0}) for thread {1}", + new Object[] {deserialize(samplingProfiler, traceContextBuffer), threadId}); } samplingProfiler.profiledThreads.remove(threadId); try { - callTree.end( - samplingProfiler.callTreePool, samplingProfiler.getInferredSpansMinDurationNs()); - int createdSpans = callTree.spanify(); - if (logger.isDebugEnabled()) { + callTree.end(samplingProfiler.callTreePool, + samplingProfiler.getInferredSpansMinDurationNs()); + int createdSpans = callTree.spanify(samplingProfiler.getClock(), + samplingProfiler.tracerProvider.get()); + if (logger.isLoggable(Level.FINE)) { if (createdSpans > 0) { - logger.debug("Created spans ({}) for thread {}", createdSpans, threadId); + logger.log(Level.FINE, "Created spans ({0}) for thread {1}", + new Object[] {createdSpans, threadId}); } else { - logger.debug( - "Created no spans for thread {} (count={})", threadId, callTree.getCount()); + logger.log(Level.FINE, "Created no spans for thread {0} (count={1})", + new Object[] {threadId, + callTree.getCount()}); } } } finally { @@ -1047,8 +945,6 @@ private void stopProfiling(SamplingProfiler samplingProfiler) { public void serialize(ByteBuffer buf) { buf.putLong(timestamp); - buf.putShort(getServiceNameIndex()); - buf.putShort(getServiceVersionIndex()); buf.put(traceContextBuffer); buf.put(previousContextBuffer); buf.put(rootContext ? (byte) 1 : (byte) 0); @@ -1058,8 +954,6 @@ public void serialize(ByteBuffer buf) { public void deserialize(ByteBuffer buf) { timestamp = buf.getLong(); - serviceName = serviceNameBackMap.get(buf.getShort()); - serviceVersion = serviceVersionBackMap.get(buf.getShort()); buf.get(traceContextBuffer); buf.get(previousContextBuffer); rootContext = buf.get() == 1; @@ -1067,45 +961,27 @@ public void deserialize(ByteBuffer buf) { activation = buf.get() == 1; } - private short getServiceNameIndex() { - Short index = serviceNameMap.get(serviceName); - if (index == null) { - index = (short) serviceNameMap.size(); - serviceNameMap.put(serviceName, index); - serviceNameBackMap.put(index, serviceName); - } - return index; - } - - private short getServiceVersionIndex() { - Short index = serviceVersionMap.get(serviceVersion); - if (index == null) { - index = (short) serviceVersionMap.size(); - serviceVersionMap.put(serviceVersion, index); - serviceVersionBackMap.put(index, serviceVersion); - } - return index; - } } /** - * Does not wait but immediately returns the highest sequence which is available for read We never - * want to wait until new elements are available, we just want to process all available events + * Does not wait but immediately returns the highest sequence which is available for read + * We never want to wait until new elements are available, + * we just want to process all available events */ private static class NoWaitStrategy implements WaitStrategy { @Override - public long waitFor( - long sequence, Sequence cursor, Sequence dependentSequence, SequenceBarrier barrier) { + public long waitFor(long sequence, Sequence cursor, Sequence dependentSequence, + SequenceBarrier barrier) { return dependentSequence.get(); } @Override - public void signalAllWhenBlocking() {} + public void signalAllWhenBlocking() { + } } - // extracting to a class instead of instantiating an anonymous inner class makes a huge difference - // in allocations + // extracting to a class instead of instantiating an anonymous inner class makes a huge difference in allocations private class WriteActivationEventToFileHandler implements EventPoller.Handler { @Override public boolean onEvent(ActivationEvent event, long sequence, boolean endOfBatch) diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java new file mode 100644 index 000000000..0c44d6303 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler; + +import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; + +public class SpanAnchoredNanoClock implements NanoClock { + private final WeakConcurrentMap nanoTimeOffsetMap = new WeakConcurrentMap<>(false); + + public void onSpanStart(ReadWriteSpan started, Context parentContext) { + Span parent = Span.fromContext(parentContext); + Long parentAnchor = parent == null ? null : nanoTimeOffsetMap.get(parent); + if (parentAnchor != null) { + nanoTimeOffsetMap.put(started, parentAnchor); + } else { + long spanLatency = started.getLatencyNanos(); + long clockNowNanos = nanoTime(); + long spanStartNanos = started.toSpanData().getStartEpochNanos(); + long anchor = spanStartNanos - spanLatency - clockNowNanos; + nanoTimeOffsetMap.put(started, anchor); + } + } + + @Override + public long nanoTime() { + return System.nanoTime(); + } + + @Override + public long getAnchor(Span span) { + return nanoTimeOffsetMap.get(span); + } + + @Override + public long toEpochNanos(long anchor, long recordedNanoTime) { + return recordedNanoTime + anchor; + } + + @Override + public void periodicCleanup() { + nanoTimeOffsetMap.expungeStaleEntries(); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java new file mode 100644 index 000000000..0eea90220 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java @@ -0,0 +1,86 @@ +package co.elastic.apm.otel.profiler; + +import java.util.Objects; +import javax.annotation.Nullable; + +public class StackFrame { + @Nullable + private final String className; + private final String methodName; + + public static StackFrame of(@Nullable String className, String methodName) { + return new StackFrame(className, methodName); + } + + public StackFrame(@Nullable String className, String methodName) { + this.className = className; + this.methodName = methodName; + } + + @Nullable + public String getClassName() { + return className; + } + + public String getMethodName() { + return methodName; + } + + public int getSimpleClassNameOffset() { + if (className != null) { + return className.lastIndexOf('.') + 1; + } + return 0; + } + + public void appendFileName(StringBuilder replaceBuilder) { + if (className != null) { + int fileNameEnd = className.indexOf('$'); + if (fileNameEnd < 0) { + fileNameEnd = className.length(); + } + int classNameStart = className.lastIndexOf('.'); + if (classNameStart < fileNameEnd && fileNameEnd <= className.length()) { + replaceBuilder.append(className, classNameStart + 1, fileNameEnd); + replaceBuilder.append(".java"); + } else { + replaceBuilder.append(""); + } + } else { + replaceBuilder.append(""); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + StackFrame that = (StackFrame) o; + + if (!Objects.equals(className, that.className)) { + return false; + } + return methodName.equals(that.methodName); + } + + @Override + public int hashCode() { + int result = className != null ? className.hashCode() : 0; + result = 31 * result + methodName.hashCode(); + return result; + } + + @Override + public String toString() { + if (className == null) { + return methodName; + } + return className + '.' + methodName; + } + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SystemNanoClock.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SystemNanoClock.java deleted file mode 100644 index 53325cc48..000000000 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SystemNanoClock.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.agent.profiler; - -public class SystemNanoClock implements NanoClock { - @Override - public long nanoTime() { - return System.nanoTime(); - } -} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java index 7341eee5c..2a5a614db 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; public class ThreadMatcher { @@ -31,19 +31,16 @@ public ThreadMatcher() { systemThreadGroup = threadGroup; } - public void forEachThread( - NonCapturingPredicate predicate, - S1 state1, - NonCapturingConsumer consumer, - S2 state2) { + public void forEachThread(NonCapturingPredicate predicate, S1 state1, + NonCapturingConsumer consumer, S2 state2) { int count = systemThreadGroup.activeCount(); do { int expectedArrayLength = count + (count / 2) + 1; if (threads.length < expectedArrayLength) { - threads = new Thread[expectedArrayLength]; // slightly grow the array size + threads = new Thread[expectedArrayLength]; //slightly grow the array size } count = systemThreadGroup.enumerate(threads, true); - // return value of enumerate() must be strictly less than the array size according to javadoc + //return value of enumerate() must be strictly less than the array size according to javadoc } while (count >= threads.length); for (int i = 0; i < count; i++) { @@ -62,4 +59,5 @@ interface NonCapturingPredicate { interface NonCapturingConsumer { void accept(T t, S state); } + } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java new file mode 100644 index 000000000..855e57823 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java @@ -0,0 +1,144 @@ +package co.elastic.apm.otel.profiler; + +import co.elastic.apm.otel.profiler.util.ByteUtils; +import co.elastic.apm.otel.profiler.util.HexUtils; +import co.elastic.apm.otel.profiler.pooling.Recyclable; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import javax.annotation.Nullable; + +/** + * A mutable (and therefore recyclable) class storing the relevant bits of {@link SpanContext} + * for generating inferred spans. Also stores a clock-anchor for the corresponding span obtained + * via {@link NanoClock#getAnchor(Span)}. + */ +public class TraceContext implements Recyclable { + + public static final int SERIALIZED_LENGTH = 16 + 8 + 1 + 8; + private long traceIdLow; + private long traceIdHigh; + private long id; + private byte flags; + + private long clockAnchor; + + public TraceContext() { + } + + // For testing only + static TraceContext fromSpanContextWithZeroClockAnchor(SpanContext ctx) { + TraceContext result = new TraceContext(); + result.filLFromSpanContext(ctx); + result.clockAnchor = 0L; + return result; + } + + private void filLFromSpanContext(SpanContext ctx) { + id = HexUtils.hexToLong(ctx.getSpanId(), 0); + traceIdHigh = HexUtils.hexToLong(ctx.getTraceId(), 0); + traceIdLow = HexUtils.hexToLong(ctx.getTraceId(), 16); + flags = ctx.getTraceFlags().asByte(); + + } + + public SpanContext toOtelSpanContext(StringBuilder temporaryBuilder) { + temporaryBuilder.setLength(0); + HexUtils.appendLongAsHex(traceIdHigh, temporaryBuilder); + HexUtils.appendLongAsHex(traceIdLow, temporaryBuilder); + String traceIdStr = temporaryBuilder.toString(); + + temporaryBuilder.setLength(0); + HexUtils.appendLongAsHex(id, temporaryBuilder); + String idStr = temporaryBuilder.toString(); + + return SpanContext.create( + traceIdStr, + idStr, + TraceFlags.fromByte(flags), + TraceState.getDefault() + ); + } + + public boolean idEquals(@Nullable TraceContext o) { + if (o == null) { + return false; + } + return id == o.id; + } + + public static long getSpanId(byte[] serialized) { + return ByteUtils.getLong(serialized, 16); + } + + public void deserialize(byte[] serialized) { + traceIdLow = ByteUtils.getLong(serialized, 0); + traceIdHigh = ByteUtils.getLong(serialized, 8); + id = ByteUtils.getLong(serialized, 16); + flags = serialized[24]; + clockAnchor = ByteUtils.getLong(serialized, 25); + } + + + public boolean traceIdAndIdEquals(byte[] otherSerialized) { + long otherTraceIdLow = ByteUtils.getLong(otherSerialized, 0); + if (otherTraceIdLow != traceIdLow) { + return false; + } + long otherTraceIdHigh = ByteUtils.getLong(otherSerialized, 8); + if (otherTraceIdHigh != traceIdHigh) { + return false; + } + long otherId = ByteUtils.getLong(otherSerialized, 16); + return id == otherId; + } + + public static void serialize(SpanContext ctx, long clockAnchor, byte[] buffer) { + long id = HexUtils.hexToLong(ctx.getSpanId(), 0); + long traceIdHigh = HexUtils.hexToLong(ctx.getTraceId(), 0); + long traceIdLow = HexUtils.hexToLong(ctx.getTraceId(), 16); + byte flags = ctx.getTraceFlags().asByte(); + ByteUtils.putLong(buffer, 0, traceIdLow); + ByteUtils.putLong(buffer, 8, traceIdHigh); + ByteUtils.putLong(buffer, 16, id); + buffer[24] = flags; + ByteUtils.putLong(buffer, 25, clockAnchor); + } + + public void serialize(byte[] buffer) { + ByteUtils.putLong(buffer, 0, traceIdLow); + ByteUtils.putLong(buffer, 8, traceIdHigh); + ByteUtils.putLong(buffer, 16, id); + buffer[24] = flags; + ByteUtils.putLong(buffer, 25, clockAnchor); + } + + public byte[] serialize() { + byte[] result = new byte[SERIALIZED_LENGTH]; + serialize(result); + return result; + } + + @Override + public void resetState() { + traceIdLow = 0; + traceIdHigh = 0; + id = 0; + flags = 0; + clockAnchor = 0; + } + + public long getClockAnchor() { + return clockAnchor; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + SpanContext otelSpanCtx = toOtelSpanContext(result); + result.setLength(0); + result.append(otelSpanCtx).append("(clock-anchor: ").append(clockAnchor).append(')'); + return result.toString(); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java index 391548f99..9b81cdc6f 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java @@ -16,32 +16,48 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.asyncprofiler; +/* + * Copyright 2018 Andrei Pangin + * + * 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 co.elastic.apm.otel.profiler.asyncprofiler; -import co.elastic.apm.agent.common.JvmRuntimeInfo; -import co.elastic.apm.agent.common.util.ResourceExtractionUtil; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import javax.annotation.Nullable; /** - * Java API for in-process profiling. Serves as a wrapper around async-profiler native library. This - * class is a singleton. The first call to {@link #getInstance(String, int)} initiates loading of + * Java API for in-process profiling. Serves as a wrapper around + * async-profiler native library. This class is a singleton. + * The first call to {@link #getInstance(String, int)} initiates loading of * libasyncProfiler.so. - * - *

    This is based on - * https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/java/one/profiler/AsyncProfiler.java, - * under Apache License 2.0. It is modified to allow it to be shaded into the {@code co.elastic.apm} - * namespace + *

    + * This is based on https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/java/one/profiler/AsyncProfiler.java, + * under Apache License 2.0. + * It is modified to allow it to be shaded into the {@code co.elastic.apm} namespace + *

    */ public class AsyncProfiler { public static final String SAFEMODE_SYSTEM_PROPERTY_NAME = "AsyncProfiler.safemode"; - @Nullable private static volatile AsyncProfiler instance; + @Nullable + private static volatile AsyncProfiler instance; - private AsyncProfiler() {} + private AsyncProfiler() { + } public static AsyncProfiler getInstance(String profilerLibDirectory, int safemode) { AsyncProfiler result = AsyncProfiler.instance; @@ -50,32 +66,25 @@ public static AsyncProfiler getInstance(String profilerLibDirectory, int safemod } synchronized (AsyncProfiler.class) { if (instance == null) { - if (JvmRuntimeInfo.ofCurrentVM().isJ9VM()) { + if (System.getProperty("java.vm.name").contains("J9")) { throw new IllegalStateException( - "OpenJ9 JVMs are not supported by async profiler. Please set " - + "profiling_inferred_spans_enabled to false"); + "OpenJ9 JVMs are not supported by async profiler. Please set " + + "profiling_inferred_spans_enabled to false"); } try { - // set the AsyncProfiler.safemode system property with the configured safemode, so that - // optimizations - // can be applied already at load time. Specifically, if (safemode & 14) == 14 (2, 4 and 8 - // bits are set), then - // async profiler will avoid enabling CompiledMethodLoad events at load time, so to - // workaround a relatd JVM bug - // (https://bugs.openjdk.java.net/browse/JDK-8202883, - // https://bugs.openjdk.java.net/browse/JDK-8173361 and friends). - // safemode can still be set for each profiling session, but it can only be stricter than - // the safemode + // set the AsyncProfiler.safemode system property with the configured safemode, so that optimizations + // can be applied already at load time. Specifically, if (safemode & 14) == 14 (2, 4 and 8 bits are set), then + // async profiler will avoid enabling CompiledMethodLoad events at load time, so to workaround a relatd JVM bug + // (https://bugs.openjdk.java.net/browse/JDK-8202883, https://bugs.openjdk.java.net/browse/JDK-8173361 and friends). + // safemode can still be set for each profiling session, but it can only be stricter than the safemode // configured at load time. System.setProperty(SAFEMODE_SYSTEM_PROPERTY_NAME, String.valueOf(safemode)); loadNativeLibrary(profilerLibDirectory); } catch (UnsatisfiedLinkError e) { - throw new IllegalStateException( - String.format( - "It is likely that %s is not an executable location. Consider setting " - + "the profiling_inferred_spans_lib_directory property to a directory on a partition that allows execution", - profilerLibDirectory), - e); + throw new IllegalStateException(String.format( + "It is likely that %s is not an executable location. Consider setting " + + "the profiling_inferred_spans_lib_directory property to a directory on a partition that allows execution", + profilerLibDirectory), e); } instance = new AsyncProfiler(); @@ -92,12 +101,8 @@ static void reset() { private static void loadNativeLibrary(String libraryDirectory) { String libraryName = getLibraryFileName(); - Path file = - ResourceExtractionUtil.extractResourceToDirectory( - "asyncprofiler/" + libraryName + ".so", - libraryName, - ".so", - Paths.get(libraryDirectory)); + Path file = ResourceExtractionUtil.extractResourceToDirectory( + "asyncprofiler/" + libraryName + ".so", libraryName, ".so", Paths.get(libraryDirectory)); System.load(file.toString()); } @@ -137,8 +142,8 @@ public void stop() throws IllegalStateException { } /** - * Execute an agent-compatible profiling command - the comma-separated list of arguments described - * in arguments.cpp + * Execute an agent-compatible profiling command - + * the comma-separated list of arguments described in arguments.cpp * * @param command Profiling command * @return The command result @@ -169,12 +174,16 @@ public void disableProfilingThread(Thread thread) throws IllegalStateException { filterThread(thread, false); } - /** Adds the current thread to the set of profiled threads */ + /** + * Adds the current thread to the set of profiled threads + */ public void enableProfilingCurrentThread() { filterThread0(null, true); } - /** Removes the current thread to the set of profiled threads */ + /** + * Removes the current thread to the set of profiled threads + */ public void disableProfilingCurrentThread() throws IllegalStateException { filterThread0(null, false); } @@ -199,4 +208,5 @@ private native void start0(String event, long interval, boolean reset) private native String execute0(String command) throws IllegalArgumentException, IOException; private native void filterThread0(Thread thread, boolean enable); + } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java index 20c866561..490ce7c64 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java @@ -16,9 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.asyncprofiler; +package co.elastic.apm.otel.profiler.asyncprofiler; -import co.elastic.apm.agent.tracer.pooling.Recyclable; + +import co.elastic.apm.otel.profiler.pooling.Recyclable; import java.io.File; import java.io.IOException; import java.nio.Buffer; @@ -29,22 +30,25 @@ import javax.annotation.Nullable; /** - * An abstraction similar to {@link MappedByteBuffer} that allows to read the content of a file with - * an API that is similar to {@link ByteBuffer}. - * - *

    Instances of this class hold a reusable buffer that contains a subset of the file, or the - * whole file if the buffer's capacity is greater or equal to the file's size. - * - *

    Whenever calling a method like {@link #getLong()} or {@link #position(long)} would exceed the - * currently buffered range the same buffer is filled with a different range of the file. - * - *

    The downside of {@link MappedByteBuffer} (and the reason for implementing this abstraction) is - * that calling methods like {@link MappedByteBuffer#get()} can increase time-to-safepoint. This is - * because these methods are implemented as JVM intrinsics. When the JVM executes an intrinsic, it - * does not switch to the native execution context which means that it's not ready to enter a - * safepoint whenever a intrinsic runs. As reading a file from disk can get stuck (for example when - * the disk is busy) calling {@link MappedByteBuffer#get()} may take a while to execute. While it's - * executing other threads have to wait for it to finish if the JVM wants to reach a safe point. + * An abstraction similar to {@link MappedByteBuffer} that allows to read the content of a file with an API that is similar to + * {@link ByteBuffer}. + *

    + * Instances of this class hold a reusable buffer that contains a subset of the file, + * or the whole file if the buffer's capacity is greater or equal to the file's size. + *

    + *

    + * Whenever calling a method like {@link #getLong()} or {@link #position(long)} would exceed the currently buffered range + * the same buffer is filled with a different range of the file. + *

    + *

    + * The downside of {@link MappedByteBuffer} (and the reason for implementing this abstraction) + * is that calling methods like {@link MappedByteBuffer#get()} can increase time-to-safepoint. + * This is because these methods are implemented as JVM intrinsics. + * When the JVM executes an intrinsic, it does not switch to the native execution context which means that it's not ready to enter a safepoint + * whenever a intrinsic runs. + * As reading a file from disk can get stuck (for example when the disk is busy) calling {@link MappedByteBuffer#get()} may take a while to execute. + * While it's executing other threads have to wait for it to finish if the JVM wants to reach a safe point. + *

    */ class BufferedFile implements Recyclable { @@ -55,17 +59,18 @@ class BufferedFile implements Recyclable { private ByteBuffer buffer; private final ByteBuffer bigBuffer; private final ByteBuffer smallBuffer; - - /** The offset of the file from where the {@link #buffer} starts */ + /** + * The offset of the file from where the {@link #buffer} starts + */ private long offset; - private boolean wholeFileInBuffer; - @Nullable private FileChannel fileChannel; + @Nullable + private FileChannel fileChannel; /** * @param bigBuffer the buffer to be used to read the whole file if the file fits into it - * @param smallBuffer the buffer to be used to read chunks of the file in case the file is larger - * than bigBuffer. Constantly seeking a file with a large buffer is very bad for performance. + * @param smallBuffer the buffer to be used to read chunks of the file in case the file is larger than bigBuffer. + * Constantly seeking a file with a large buffer is very bad for performance. */ public BufferedFile(ByteBuffer bigBuffer, ByteBuffer smallBuffer) { this.bigBuffer = bigBuffer; @@ -73,8 +78,7 @@ public BufferedFile(ByteBuffer bigBuffer, ByteBuffer smallBuffer) { } /** - * Sets the file and depending on it's size, may read the file into the {@linkplain #buffer - * buffer} + * Sets the file and depending on it's size, may read the file into the {@linkplain #buffer buffer} * * @param file the file to read from * @throws IOException If some I/O error occurs @@ -132,8 +136,7 @@ public void position(long pos) { /** * Ensures that the provided number of bytes are available in the {@linkplain #buffer buffer} * - * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain - * #buffer buffer} + * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain #buffer buffer} * @throws IOException If some I/O error occurs * @throws IllegalStateException If minRemaining is greater than the buffer's capacity */ @@ -144,10 +147,8 @@ public void ensureRemaining(int minRemaining) throws IOException { /** * Ensures that the provided number of bytes are available in the {@linkplain #buffer buffer} * - * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain - * #buffer buffer} - * @param maxRead the max number of bytes to read from the file in case the buffer does currently - * not hold {@code minRemaining} bytes + * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain #buffer buffer} + * @param maxRead the max number of bytes to read from the file in case the buffer does currently not hold {@code minRemaining} bytes * @throws IOException If some I/O error occurs * @throws IllegalStateException If minRemaining is greater than the buffer's capacity */ @@ -157,8 +158,8 @@ public void ensureRemaining(int minRemaining, int maxRead) throws IOException { } if (minRemaining > buffer.capacity()) { throw new IllegalStateException( - String.format( - "Length (%d) greater than buffer capacity (%d)", minRemaining, buffer.capacity())); + String.format("Length (%d) greater than buffer capacity (%d)", minRemaining, + buffer.capacity())); } if (buffer.remaining() < minRemaining) { read(position(), maxRead); @@ -166,9 +167,8 @@ public void ensureRemaining(int minRemaining, int maxRead) throws IOException { } /** - * Gets a byte from the current {@linkplain #position() position} of this file. If the {@linkplain - * #buffer buffer} does not fully contain this byte, loads another slice of the file into the - * buffer. + * Gets a byte from the current {@linkplain #position() position} of this file. + * If the {@linkplain #buffer buffer} does not fully contain this byte, loads another slice of the file into the buffer. * * @return The byte at the file's current position * @throws IOException If some I/O error occurs @@ -179,9 +179,8 @@ public short get() throws IOException { } /** - * Gets a short from the current {@linkplain #position() position} of this file. If the - * {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file - * into the buffer. + * Gets a short from the current {@linkplain #position() position} of this file. + * If the {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file into the buffer. * * @return The short at the file's current position * @throws IOException If some I/O error occurs @@ -192,9 +191,8 @@ public short getShort() throws IOException { } /** - * Gets a short from the current {@linkplain #position() position} of this file. If the - * {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file - * into the buffer. + * Gets a short from the current {@linkplain #position() position} of this file. + * If the {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file into the buffer. * * @return The short at the file's current position * @throws IOException If some I/O error occurs @@ -204,9 +202,8 @@ public int getUnsignedShort() throws IOException { } /** - * Gets a int from the current {@linkplain #position() position} of this file and converts it to - * an unsigned short. If the {@linkplain #buffer buffer} does not fully contain this int, loads - * another slice of the file into the buffer. + * Gets a int from the current {@linkplain #position() position} of this file and converts it to an unsigned short. + * If the {@linkplain #buffer buffer} does not fully contain this int, loads another slice of the file into the buffer. * * @return The int at the file's current position * @throws IOException If some I/O error occurs @@ -217,9 +214,8 @@ public int getInt() throws IOException { } /** - * Gets a long from the current {@linkplain #position() position} of this file. If the {@linkplain - * #buffer buffer} does not fully contain this long, loads another slice of the file into the - * buffer. + * Gets a long from the current {@linkplain #position() position} of this file. + * If the {@linkplain #buffer buffer} does not fully contain this long, loads another slice of the file into the buffer. * * @return The long at the file's current position * @throws IOException If some I/O error occurs @@ -230,56 +226,52 @@ public long getLong() throws IOException { } /** - * Gets a byte from the underlying buffer without checking if this part of the file is actually in - * the buffer. - * - *

    Always mare sure to call {@link #ensureRemaining} before. + * Gets a byte from the underlying buffer without checking if this part of the file is actually in the buffer. + *

    + * Always mare sure to call {@link #ensureRemaining} before. + *

    * * @return The byte at the file's current position - * @throws java.nio.BufferUnderflowException If the buffer's current position is not smaller than - * its limit + * @throws java.nio.BufferUnderflowException If the buffer's current position is not smaller than its limit */ public byte getUnsafe() { return buffer.get(); } /** - * Gets a short from the underlying buffer without checking if this part of the file is actually - * in the buffer. - * - *

    Always mare sure to call {@link #ensureRemaining} before. + * Gets a short from the underlying buffer without checking if this part of the file is actually in the buffer. + *

    + * Always mare sure to call {@link #ensureRemaining} before. + *

    * * @return The byte at the file's current position - * @throws java.nio.BufferUnderflowException If there are fewer than two bytes remaining in this - * buffer + * @throws java.nio.BufferUnderflowException If there are fewer than two bytes remaining in this buffer */ public short getUnsafeShort() { return buffer.getShort(); } /** - * Gets an int from the underlying buffer without checking if this part of the file is actually in - * the buffer. - * - *

    Always mare sure to call {@link #ensureRemaining} before. + * Gets an int from the underlying buffer without checking if this part of the file is actually in the buffer. + *

    + * Always mare sure to call {@link #ensureRemaining} before. + *

    * * @return The byte at the file's current position - * @throws java.nio.BufferUnderflowException If there are fewer than four bytes remaining in this - * buffer + * @throws java.nio.BufferUnderflowException If there are fewer than four bytes remaining in this buffer */ public int getUnsafeInt() { return buffer.getInt(); } /** - * Gets a long from the underlying buffer without checking if this part of the file is actually in - * the buffer. - * - *

    Always mare sure to call {@link #ensureRemaining} before. + * Gets a long from the underlying buffer without checking if this part of the file is actually in the buffer. + *

    + * Always mare sure to call {@link #ensureRemaining} before. + *

    * * @return The byte at the file's current position - * @throws java.nio.BufferUnderflowException If there are fewer than eight bytes remaining in this - * buffer + * @throws java.nio.BufferUnderflowException If there are fewer than eight bytes remaining in this buffer */ public long getUnsafeLong() { return buffer.getLong(); diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java index 34023c905..08c3a0824 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java @@ -16,17 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.asyncprofiler; +package co.elastic.apm.otel.profiler.asyncprofiler; -import co.elastic.apm.agent.common.util.WildcardMatcher; -import co.elastic.apm.agent.impl.transaction.StackFrame; -import co.elastic.apm.agent.profiler.collections.Int2IntHashMap; -import co.elastic.apm.agent.profiler.collections.Int2ObjectHashMap; -import co.elastic.apm.agent.profiler.collections.Long2LongHashMap; -import co.elastic.apm.agent.profiler.collections.Long2ObjectHashMap; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; -import co.elastic.apm.agent.tracer.pooling.Recyclable; +import co.elastic.apm.otel.profiler.StackFrame; +import co.elastic.apm.otel.profiler.collections.Int2ObjectHashMap; +import co.elastic.apm.otel.profiler.collections.Long2LongHashMap; +import co.elastic.apm.otel.profiler.collections.Long2ObjectHashMap; +import co.elastic.apm.otel.profiler.collections.Int2IntHashMap; +import co.elastic.apm.otel.profiler.config.WildcardMatcher; +import co.elastic.apm.otel.profiler.pooling.Recyclable; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; @@ -35,29 +33,32 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nullable; /** - * Parses the binary JFR file created by async-profiler. May not work with JFR files created by an - * actual flight recorder. - * - *

    The implementation is tuned with to minimize allocations when parsing a JFR file. Most data - * structures can be reused by first {@linkplain #resetState() resetting the state} and then - * {@linkplain #parse(File, List, List) parsing} another file. + * Parses the binary JFR file created by async-profiler. + * May not work with JFR files created by an actual flight recorder. + *

    + * The implementation is tuned with to minimize allocations when parsing a JFR file. + * Most data structures can be reused by first {@linkplain #resetState() resetting the state} and then {@linkplain #parse(File, List, List) parsing} + * another file. + *

    */ public class JfrParser implements Recyclable { - private static final Logger logger = LoggerFactory.getLogger(JfrParser.class); + private static final Logger logger = Logger.getLogger(JfrParser.class.getName()); private static final byte[] MAGIC_BYTES = new byte[] {'F', 'L', 'R', '\0'}; - private static final Set JAVA_FRAME_TYPES = - new HashSet<>(Arrays.asList("Interpreted", "JIT compiled", "Inlined")); + private static final Set JAVA_FRAME_TYPES = new HashSet<>( + Arrays.asList("Interpreted", "JIT compiled", "Inlined")); private static final int BIG_FILE_BUFFER_SIZE = 5 * 1024 * 1024; private static final int SMALL_FILE_BUFFER_SIZE = 4 * 1024; private static final String SYMBOL_EXCLUDED = "3x cluded"; private static final String SYMBOL_NULL = "n u11"; - private static final StackFrame FRAME_EXCLUDED = new StackFrame("excluded", "excluded"); - private static final StackFrame FRAME_NULL = new StackFrame("null", "null"); + private final static StackFrame FRAME_EXCLUDED = new StackFrame("excluded", "excluded"); + private final static StackFrame FRAME_NULL = new StackFrame("null", "null"); private final BufferedFile bufferedFile; private final Int2IntHashMap classIdToClassNameSymbolId = new Int2IntHashMap(-1); @@ -65,21 +66,22 @@ public class JfrParser implements Recyclable { private final Int2ObjectHashMap symbolIdToString = new Int2ObjectHashMap(); private final Int2IntHashMap stackTraceIdToFilePositions = new Int2IntHashMap(-1); private final Long2LongHashMap nativeTidToJavaTid = new Long2LongHashMap(-1); - private final Long2ObjectHashMap frameIdToFrame = - new Long2ObjectHashMap(); + private final Long2ObjectHashMap frameIdToFrame = new Long2ObjectHashMap(); private final Long2LongHashMap frameIdToMethodSymbol = new Long2LongHashMap(-1); private final Long2LongHashMap frameIdToClassId = new Long2LongHashMap(-1); // used to resolve a symbol with minimal allocations private final StringBuilder symbolBuilder = new StringBuilder(); private long eventsOffset; private long metadataOffset; - @Nullable private boolean[] isJavaFrameType; - @Nullable private List excludedClasses; - @Nullable private List includedClasses; + @Nullable + private boolean[] isJavaFrameType; + @Nullable + private List excludedClasses; + @Nullable + private List includedClasses; public JfrParser() { - this( - ByteBuffer.allocateDirect(BIG_FILE_BUFFER_SIZE), + this(ByteBuffer.allocateDirect(BIG_FILE_BUFFER_SIZE), ByteBuffer.allocateDirect(SMALL_FILE_BUFFER_SIZE)); } @@ -88,29 +90,27 @@ public JfrParser() { } /** - * Initializes the parser to make it ready for {@link #resolveStackTrace(long, boolean, List, - * int)} to be called. + * Initializes the parser to make it ready for {@link #resolveStackTrace(long, boolean, List, int)} to be called. * * @param file the JFR file to parse - * @param excludedClasses Class names to exclude in stack traces (has an effect on {@link - * #resolveStackTrace(long, boolean, List, int)}) - * @param includedClasses Class names to include in stack traces (has an effect on {@link - * #resolveStackTrace(long, boolean, List, int)}) + * @param excludedClasses Class names to exclude in stack traces (has an effect on {@link #resolveStackTrace(long, boolean, List, int)}) + * @param includedClasses Class names to include in stack traces (has an effect on {@link #resolveStackTrace(long, boolean, List, int)}) * @throws IOException if some I/O error occurs */ - public void parse( - File file, List excludedClasses, List includedClasses) - throws IOException { + public void parse(File file, List excludedClasses, + List includedClasses) throws IOException { this.excludedClasses = excludedClasses; this.includedClasses = includedClasses; bufferedFile.setFile(file); long fileSize = bufferedFile.size(); if (fileSize < 16) { throw new IllegalStateException( - "Unexpected sampling profiler error, everything else should work as expected. " - + "Please report to us with as many details, including OS and JVM details."); + "Unexpected sampling profiler error, everything else should work as expected. " + + "Please report to us with as many details, including OS and JVM details."); + } + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Parsing {0} ({1} bytes)", new Object[] {file, fileSize}); } - logger.debug("Parsing {} ({} bytes)", file, fileSize); bufferedFile.ensureRemaining(16, 16); for (byte magicByte : MAGIC_BYTES) { if (bufferedFile.get() != magicByte) { @@ -149,7 +149,7 @@ private void expectEventType(int expectedEventType) throws IOException { private void parseCheckpoint(long checkpointOffset) throws IOException { bufferedFile.position(checkpointOffset); - int size = bufferedFile.getInt(); // size + int size = bufferedFile.getInt();// size expectEventType(EventTypeId.EVENT_CHECKPOINT); bufferedFile.getLong(); // stop timestamp bufferedFile.getLong(); // previous checkpoint - always 0 in async-profiler @@ -161,7 +161,9 @@ private void parseCheckpoint(long checkpointOffset) throws IOException { private void parseContent() throws IOException { BufferedFile bufferedFile = this.bufferedFile; int contentTypeId = bufferedFile.getInt(); - logger.debug("Parsing content type {}", contentTypeId); + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Parsing content type {0}", contentTypeId); + } int count = bufferedFile.getInt(); switch (contentTypeId) { case ContentTypeId.CONTENT_THREAD: @@ -203,8 +205,7 @@ private void parseContent() throws IOException { // classId is an incrementing integer, no way there are more than 2 billion distinct ones int classId = (int) bufferedFile.getUnsafeLong(); bufferedFile.getUnsafeLong(); // loader class - // symbol ids are incrementing integers, no way there are more than 2 billion distinct - // ones + // symbol ids are incrementing integers, no way there are more than 2 billion distinct ones int classNameSymbolId = (int) bufferedFile.getUnsafeLong(); classIdToClassNameSymbolId.put(classId, classNameSymbolId); // class name bufferedFile.getUnsafeShort(); // access flags @@ -216,8 +217,7 @@ private void parseContent() throws IOException { long id = bufferedFile.getUnsafeLong(); // classId is an incrementing integer, no way there are more than 2 billion distinct ones int classId = (int) bufferedFile.getUnsafeLong(); - // symbol ids are incrementing integers, no way there are more than 2 billion distinct - // ones + // symbol ids are incrementing integers, no way there are more than 2 billion distinct ones int methodNameSymbolId = (int) bufferedFile.getUnsafeLong(); frameIdToFrame.put(id, FRAME_NULL); frameIdToClassId.put(id, classId); @@ -229,8 +229,7 @@ private void parseContent() throws IOException { break; case ContentTypeId.CONTENT_SYMBOL: for (int i = 0; i < count; i++) { - // symbol ids are incrementing integers, no way there are more than 2 billion distinct - // ones + // symbol ids are incrementing integers, no way there are more than 2 billion distinct ones int symbolId = (int) bufferedFile.getLong(); int pos = (int) bufferedFile.position(); symbolIdToPos.put(symbolId, pos); @@ -301,30 +300,27 @@ public void consumeStackTraces(StackTraceConsumer callback) throws IOException { /** * Resolves the stack trace with the given {@code stackTraceId}. + *

    + * Note that his allocates strings for symbols in case a stack frame has not already been resolved for the current JFR file yet. + * These strings are currently not cached so this can create some GC pressure. + *

    + *

    + * Excludes frames based on the {@link WildcardMatcher}s supplied to {@link #parse(File, List, List)}. + *

    * - *

    Note that his allocates strings for symbols in case a stack frame has not already been - * resolved for the current JFR file yet. These strings are currently not cached so this can - * create some GC pressure. - * - *

    Excludes frames based on the {@link WildcardMatcher}s supplied to {@link #parse(File, List, - * List)}. - * - * @param stackTraceId The id of the stack traced. Used to look up the position of the file in - * which the given stack trace is stored via {@link #stackTraceIdToFilePositions}. - * @param onlyJavaFrames If {@code true}, will only resolve {@code Interpreted}, {@code JIT - * compiled} and {@code Inlined} frames. If {@code false}, will also resolve {@code Native}, - * {@code Kernel} and {@code C++} frames. - * @param stackFrames The mutable list where the stack frames are written to. Don't forget to - * {@link List#clear()} the list before calling this method if the list is reused. - * @param maxStackDepth The max size of the stackFrames list (excluded frames don't take up - * space). In contrast to async-profiler's {@code jstackdepth} argument this does not truncate - * the bottom of the stack, only the top. This is important to properly create a call tree - * without making it overly complex. + * @param stackTraceId The id of the stack traced. + * Used to look up the position of the file in which the given stack trace is stored via {@link #stackTraceIdToFilePositions}. + * @param onlyJavaFrames If {@code true}, will only resolve {@code Interpreted}, {@code JIT compiled} and {@code Inlined} frames. + * If {@code false}, will also resolve {@code Native}, {@code Kernel} and {@code C++} frames. + * @param stackFrames The mutable list where the stack frames are written to. + * Don't forget to {@link List#clear()} the list before calling this method if the list is reused. + * @param maxStackDepth The max size of the stackFrames list (excluded frames don't take up space). + * In contrast to async-profiler's {@code jstackdepth} argument this does not truncate the bottom of the stack, only the top. + * This is important to properly create a call tree without making it overly complex. * @throws IOException if there is an error reading in current buffer */ - public void resolveStackTrace( - long stackTraceId, boolean onlyJavaFrames, List stackFrames, int maxStackDepth) - throws IOException { + public void resolveStackTrace(long stackTraceId, boolean onlyJavaFrames, + List stackFrames, int maxStackDepth) throws IOException { if (!bufferedFile.isSet()) { throw new IllegalStateException("getStackTrace was called before parse"); } @@ -348,9 +344,8 @@ public void resolveStackTrace( bufferedFile.position(position); } - private void addFrameIfIncluded( - List stackFrames, boolean onlyJavaFrames, long frameId, byte frameType) - throws IOException { + private void addFrameIfIncluded(List stackFrames, boolean onlyJavaFrames, + long frameId, byte frameType) throws IOException { if (!onlyJavaFrames || isJavaFrameType(frameType)) { StackFrame stackFrame = resolveStackFrame(frameId); if (stackFrame != FRAME_EXCLUDED) { @@ -390,8 +385,8 @@ private StringBuilder resolveSymbolBuilder(int pos, boolean replaceSlashWithDot) } private boolean isClassIncluded(CharSequence className) { - return WildcardMatcher.isAnyMatch(includedClasses, className) - && WildcardMatcher.isNoneMatch(excludedClasses, className); + return WildcardMatcher.isAnyMatch(includedClasses, className) && WildcardMatcher.isNoneMatch( + excludedClasses, className); } private StackFrame resolveStackFrame(long frameId) throws IOException { @@ -399,8 +394,8 @@ private StackFrame resolveStackFrame(long frameId) throws IOException { if (stackFrame != FRAME_NULL) { return stackFrame; } - String className = - resolveSymbol(classIdToClassNameSymbolId.get((int) frameIdToClassId.get(frameId)), true); + String className = resolveSymbol( + classIdToClassNameSymbolId.get((int) frameIdToClassId.get(frameId)), true); if (className == SYMBOL_EXCLUDED) { stackFrame = FRAME_EXCLUDED; } else { @@ -452,12 +447,10 @@ public void resetState() { public interface StackTraceConsumer { /** - * @param threadId The {@linkplain Thread#getId() Java thread id} for with the event was - * recorded. - * @param stackTraceId The id of the stack trace event. Can be used to resolve the stack trace - * via {@link #resolveStackTrace(long, boolean, List, int)} - * @param nanoTime The timestamp of the event which can be correlated with {@link - * System#nanoTime()} + * @param threadId The {@linkplain Thread#getId() Java thread id} for with the event was recorded. + * @param stackTraceId The id of the stack trace event. + * Can be used to resolve the stack trace via {@link #resolveStackTrace(long, boolean, List, int)} + * @param nanoTime The timestamp of the event which can be correlated with {@link System#nanoTime()} * @throws IOException if there is any error reading stack trace */ void onCallTree(long threadId, long stackTraceId, long nanoTime) throws IOException; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java new file mode 100644 index 000000000..270982c2e --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java @@ -0,0 +1,139 @@ +package co.elastic.apm.otel.profiler.asyncprofiler; + + +import static java.nio.file.LinkOption.NOFOLLOW_LINKS; +import static java.nio.file.StandardOpenOption.CREATE_NEW; +import static java.nio.file.StandardOpenOption.READ; +import static java.nio.file.StandardOpenOption.WRITE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.EnumSet; + +public class ResourceExtractionUtil { + + /** + * Extracts a classpath resource to {@code ${System.getProperty("java.io.tmpdir")}/$prefix-$hash.$suffix}. + * If the file has already been extracted it will not be extracted again. + * + * @param resource The classpath resource to extract. + * @param prefix The prefix of the extracted file. + * @param suffix The suffix of the extracted file. + * @return the extracted file. + */ + public static synchronized Path extractResourceToTempDirectory(String resource, String prefix, + String suffix) { + return extractResourceToDirectory(resource, prefix, suffix, + Paths.get(System.getProperty("java.io.tmpdir"))); + } + + /** + * Extracts a classpath resource to {@code $directory/$prefix-$userHash-$hash.$suffix}. + * If the file has already been extracted it will not be extracted again. + * + * @param resource The classpath resource to extract. + * @param prefix The prefix of the extracted file. + * @param suffix The suffix of the extracted file. + * @param directory The directory in which the file is to be created, or null if the default temporary-file directory is to be used. + * @return the extracted file. + */ + /* + * Why it's synchronized : if the same JVM try to lock file, we got an java.nio.channels.OverlappingFileLockException. + * So we need to block until the file is totally written. + */ + public static synchronized Path extractResourceToDirectory(String resource, String prefix, + String suffix, Path directory) { + try (InputStream resourceStream = ResourceExtractionUtil.class.getResourceAsStream( + "/" + resource)) { + if (resourceStream == null) { + throw new IllegalStateException(resource + " not found"); + } + UserPrincipal currentUserPrincipal = getCurrentUserPrincipal(); + // we have to include current user name as multiple copies of the same agent could be attached + // to multiple JVMs, each running under a different user. Hashing makes the name path-friendly. + String userHash = hash(currentUserPrincipal.getName()); + // to guard against re-using previous versions + String resourceHash = hash(ResourceExtractionUtil.class.getResourceAsStream("/" + resource)); + + Path tempFile = directory.resolve( + prefix + "-" + userHash.substring(0, 32) + "-" + resourceHash.substring(0, 32) + suffix); + try { + FileAttribute[] attr; + if (tempFile.getFileSystem().supportedFileAttributeViews().contains("posix")) { + attr = new FileAttribute[] { + PosixFilePermissions.asFileAttribute(EnumSet.of(OWNER_WRITE, OWNER_READ))}; + } else { + attr = new FileAttribute[0]; + } + try (FileChannel channel = FileChannel.open(tempFile, EnumSet.of(CREATE_NEW, WRITE), + attr)) { + // make other JVM instances wait until fully written + try (FileLock writeLock = channel.lock()) { + channel.transferFrom(Channels.newChannel(resourceStream), 0, Long.MAX_VALUE); + } + } + } catch (FileAlreadyExistsException e) { + try (FileChannel channel = FileChannel.open(tempFile, READ, NOFOLLOW_LINKS)) { + // wait until other JVM instances have fully written the file + // multiple JVMs can read the file at the same time + try (FileLock readLock = channel.lock(0, Long.MAX_VALUE, true)) { + if (!hash(Files.newInputStream(tempFile)).equals(resourceHash)) { + throw new IllegalStateException( + "Invalid checksum of " + tempFile + ". Please delete this file."); + } else if (!Files.getOwner(tempFile).equals(currentUserPrincipal)) { + throw new IllegalStateException( + "File " + tempFile + " is not owned by '" + currentUserPrincipal.getName() + + "'. Please delete this file."); + } + } + } + } + return tempFile.toAbsolutePath(); + } catch (NoSuchAlgorithmException | IOException e) { + throw new IllegalStateException(e); + } + } + + private static UserPrincipal getCurrentUserPrincipal() throws IOException { + Path whoami = Files.createTempFile("whoami", ".tmp"); + try { + return Files.getOwner(whoami); + } finally { + Files.delete(whoami); + } + } + + private static String hash(InputStream resourceAsStream) + throws IOException, NoSuchAlgorithmException { + try (InputStream is = resourceAsStream) { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[1024]; + DigestInputStream dis = new DigestInputStream(is, md); + while (dis.read(buffer) != -1) { + } + return new BigInteger(1, md.digest()).toString(16); + } + } + + private static String hash(String s) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(s.getBytes()); + return new BigInteger(1, md.digest()).toString(16); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/package-info.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/package-info.java deleted file mode 100644 index 776f677cb..000000000 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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. - */ -@NonnullApi -package co.elastic.apm.agent.profiler.asyncprofiler; - -import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java index ef04c0b27..ab18889d3 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java @@ -16,14 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; -/** Utility functions for collection objects. */ +/** + * Utility functions for collection objects. + */ public class CollectionUtil { /** * Validate that a load factor is in the range of 0.1 to 0.9. - * - *

    Load factors in the range 0.5 - 0.7 are recommended for open-addressing with linear probing. + *

    + * Load factors in the range 0.5 - 0.7 are recommended for open-addressing with linear probing. * * @param loadFactor to be validated. */ @@ -36,11 +53,11 @@ public static void validateLoadFactor(final float loadFactor) { /** * Fast method of finding the next power of 2 greater than or equal to the supplied value. - * - *

    If the value is <= 0 then 1 will be returned. - * - *

    This method is not suitable for {@link Integer#MIN_VALUE} or numbers greater than 2^30. When - * provided then {@link Integer#MIN_VALUE} will be returned. + *

    + * If the value is <= 0 then 1 will be returned. + *

    + * This method is not suitable for {@link Integer#MIN_VALUE} or numbers greater than 2^30. When provided + * then {@link Integer#MIN_VALUE} will be returned. * * @param value from which to search for next power of 2. * @return The next power of 2 or the value itself if it is a power of 2. @@ -51,11 +68,11 @@ public static int findNextPositivePowerOfTwo(final int value) { /** * Fast method of finding the next power of 2 greater than or equal to the supplied value. - * - *

    If the value is <= 0 then 1 will be returned. - * - *

    This method is not suitable for {@link Long#MIN_VALUE} or numbers greater than 2^62. When - * provided then {@link Long#MIN_VALUE} will be returned. + *

    + * If the value is <= 0 then 1 will be returned. + *

    + * This method is not suitable for {@link Long#MIN_VALUE} or numbers greater than 2^62. When provided + * then {@link Long#MIN_VALUE} will be returned. * * @param value from which to search for next power of 2. * @return The next power of 2 or the value itself if it is a power of 2. diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java index 8c59e567b..451f8a883 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java @@ -16,11 +16,30 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; -/** Hashing functions for applying to integers. */ +/** + * Hashing functions for applying to integers. + */ public class Hashing { - /** Default load factor to be used in open addressing hashed data structures. */ + /** + * Default load factor to be used in open addressing hashed data structures. + */ public static final float DEFAULT_LOAD_FACTOR = 0.55f; /** diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java index 2e827529b..cdab3491f 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java @@ -16,10 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; - -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; import java.io.Serializable; import java.util.AbstractCollection; @@ -30,7 +42,12 @@ import java.util.NoSuchElementException; import java.util.Objects; -/** A open addressing with linear probing hash map specialised for primitive key and value pairs. */ +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; + + +/** + * A open addressing with linear probing hash map specialised for primitive key and value pairs. + */ public class Int2IntHashMap implements Map, Serializable { static final int MIN_CAPACITY = 8; @@ -49,7 +66,10 @@ public Int2IntHashMap(final int missingValue) { this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, missingValue); } - public Int2IntHashMap(final int initialCapacity, final float loadFactor, final int missingValue) { + public Int2IntHashMap( + final int initialCapacity, + final float loadFactor, + final int missingValue) { this(initialCapacity, loadFactor, missingValue, true); } @@ -64,13 +84,13 @@ public Int2IntHashMap( final float loadFactor, final int missingValue, final boolean shouldAvoidAllocation) { - validateLoadFactor(loadFactor); + CollectionUtil.validateLoadFactor(loadFactor); this.loadFactor = loadFactor; this.missingValue = missingValue; this.shouldAvoidAllocation = shouldAvoidAllocation; - capacity(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity))); + capacity(CollectionUtil.findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity))); } /** @@ -101,8 +121,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. This is a function of the - * current capacity and load factor. + * Get the actual threshold which when reached the map will resize. + * This is a function of the current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -110,12 +130,16 @@ public int resizeThreshold() { return resizeThreshold; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return size; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return size == 0; } @@ -215,9 +239,9 @@ private void rehash(final int newCapacity) { /** * Primitive specialised forEach implementation. - * - *

    NB: Renamed from forEach to avoid overloading on parameter types of lambda expression, which - * doesn't play well with type inference in lambda expressions. + *

    + * NB: Renamed from forEach to avoid overloading on parameter types of lambda + * expression, which doesn't play well with type inference in lambda expressions. * * @param consumer a callback called for each key/value pair in the map. */ @@ -229,8 +253,8 @@ public void intForEach(final IntIntConsumer consumer) { for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { if (entries[keyIndex + 1] != missingValue) // lgtm [java/index-out-of-bounds] { - consumer.accept( - entries[keyIndex], entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] + consumer.accept(entries[keyIndex], + entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] } } } @@ -268,7 +292,9 @@ public boolean containsValue(final int value) { return found; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { if (size > 0) { Arrays.fill(entries, missingValue); @@ -277,44 +303,57 @@ public void clear() { } /** - * Compact the backing arrays by rehashing with a capacity just larger than current size and - * giving consideration to the load factor. + * Compact the backing arrays by rehashing with a capacity just larger than current size + * and giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); - rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); + rehash(CollectionUtil.findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); } // ---------------- Boxed Versions Below ---------------- - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public Integer get(final Object key) { return valOrNull(get((int) key)); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public Integer put(final Integer key, final Integer value) { return valOrNull(put((int) key, (int) value)); } - /** {@inheritDoc} */ + + /** + * {@inheritDoc} + */ public boolean containsKey(final Object key) { return containsKey((int) key); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean containsValue(final Object value) { return containsValue((int) value); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void putAll(final Map map) { for (final Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public KeySet keySet() { if (null == keySet) { keySet = new KeySet(); @@ -323,7 +362,9 @@ public KeySet keySet() { return keySet; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public ValueCollection values() { if (null == values) { values = new ValueCollection(); @@ -332,7 +373,9 @@ public ValueCollection values() { return values; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public EntrySet entrySet() { if (null == entrySet) { entrySet = new EntrySet(); @@ -341,7 +384,9 @@ public EntrySet entrySet() { return entrySet; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public Integer remove(final Object key) { return valOrNull(remove((int) key)); } @@ -385,8 +430,8 @@ private void compactChain(int deleteKeyIndex) { final int hash = Hashing.evenHash(entries[keyIndex], mask); - if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) - || (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { + if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) || + (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { entries[deleteKeyIndex] = entries[keyIndex]; entries[deleteKeyIndex + 1] = entries[keyIndex + 1]; @@ -397,8 +442,7 @@ private void compactChain(int deleteKeyIndex) { } /** - * Get the minimum value stored in the map. If the map is empty then it will return {@link - * #missingValue()} + * Get the minimum value stored in the map. If the map is empty then it will return {@link #missingValue()} * * @return the minimum value stored in the map. */ @@ -420,8 +464,7 @@ public int minValue() { } /** - * Get the maximum value stored in the map. If the map is empty then it will return {@link - * #missingValue()} + * Get the maximum value stored in the map. If the map is empty then it will return {@link #missingValue()} * * @return the maximum value stored in the map. */ @@ -442,7 +485,9 @@ public int maxValue() { return max; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public String toString() { if (isEmpty()) { return "{}"; @@ -467,8 +512,8 @@ public String toString() { * * @param key key with which the specified value is associated * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or {@link #missingValue()} if - * there was no mapping for the key. + * @return the previous value associated with the specified key, or + * {@link #missingValue()} if there was no mapping for the key. */ public int replace(final int key, final int value) { int curValue = get(key); @@ -498,7 +543,9 @@ public boolean replace(final int key, final int oldValue, final int newValue) { return true; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ @SuppressWarnings("unchecked") public boolean equals(final Object o) { if (this == o) { @@ -511,6 +558,7 @@ public boolean equals(final Object o) { final Map that = (Map) o; return size == that.size() && entrySet().equals(that.entrySet()); + } public int hashCode() { @@ -527,7 +575,8 @@ private void capacity(final int newCapacity) { throw new IllegalStateException("max capacity reached at size=" + size); } - /*@DoNotSub*/ resizeThreshold = (int) (newCapacity * loadFactor); + /*@DoNotSub*/ + resizeThreshold = (int) (newCapacity * loadFactor); entries = new int[entriesLength]; Arrays.fill(entries, missingValue); } @@ -616,7 +665,9 @@ public void remove() { } } - /** Iterator over keys which supports access to unboxed keys. */ + /** + * Iterator over keys which supports access to unboxed keys. + */ public final class KeyIterator extends AbstractIterator implements Iterator { public Integer next() { return nextValue(); @@ -629,7 +680,9 @@ public int nextValue() { } } - /** Iterator over values which supports access to unboxed values. */ + /** + * Iterator over values which supports access to unboxed values. + */ public final class ValueIterator extends AbstractIterator implements Iterator { public Integer next() { return nextValue(); @@ -642,8 +695,11 @@ public int nextValue() { } } - /** Iterator over entries which supports access to unboxed keys and values. */ - public final class EntryIterator extends AbstractIterator + /** + * Iterator over entries which supports access to unboxed keys and values. + */ + public final class EntryIterator + extends AbstractIterator implements Iterator>, Entry { public Integer getKey() { return getIntKey(); @@ -718,8 +774,8 @@ public boolean equals(final Object o) { final Entry e = (Entry) o; - return (e.getKey() != null && e.getValue() != null) - && (e.getKey().equals(k) && e.getValue().equals(v)); + return (e.getKey() != null && e.getValue() != null) && + (e.getKey().equals(k) && e.getValue().equals(v)); } public String toString() { @@ -728,12 +784,16 @@ public String toString() { }; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int hashCode() { return getIntKey() ^ getIntValue(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean equals(final Object o) { if (this == o) { return true; @@ -749,11 +809,15 @@ public boolean equals(final Object o) { } } - /** Set of keys which supports optional cached iterators to avoid allocation. */ + /** + * Set of keys which supports optional cached iterators to avoid allocation. + */ public final class KeySet extends AbstractSet implements Serializable { private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public KeyIterator iterator() { KeyIterator keyIterator = this.keyIterator; if (null == keyIterator) { @@ -765,22 +829,30 @@ public KeyIterator iterator() { return keyIterator; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return Int2IntHashMap.this.size(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return Int2IntHashMap.this.isEmpty(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { Int2IntHashMap.this.clear(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object o) { return contains((int) o); } @@ -790,11 +862,15 @@ public boolean contains(final int key) { } } - /** Collection of values which supports optionally cached iterators to avoid allocation. */ + /** + * Collection of values which supports optionally cached iterators to avoid allocation. + */ public final class ValueCollection extends AbstractCollection { private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public ValueIterator iterator() { ValueIterator valueIterator = this.valueIterator; if (null == valueIterator) { @@ -806,12 +882,16 @@ public ValueIterator iterator() { return valueIterator; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return Int2IntHashMap.this.size(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object o) { return contains((int) o); } @@ -821,11 +901,15 @@ public boolean contains(final int key) { } } - /** Set of entries which supports optionally cached iterators to avoid allocation. */ + /** + * Set of entries which supports optionally cached iterators to avoid allocation. + */ public final class EntrySet extends AbstractSet> implements Serializable { private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public EntryIterator iterator() { EntryIterator entryIterator = this.entryIterator; if (null == entryIterator) { @@ -837,22 +921,30 @@ public EntryIterator iterator() { return entryIterator; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return Int2IntHashMap.this.size(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return Int2IntHashMap.this.isEmpty(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { Int2IntHashMap.this.clear(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object o) { final Entry entry = (Entry) o; final Integer value = get(entry.getKey()); diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java index 6f01d8f2d..ee9f48c7e 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java @@ -16,11 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; - -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; -import static java.util.Objects.requireNonNull; +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; import java.io.Serializable; import java.util.AbstractCollection; @@ -31,13 +42,19 @@ import java.util.NoSuchElementException; import java.util.Objects; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; +import static java.util.Objects.requireNonNull; + + /** - * {@link Map} implementation specialised for int keys using open addressing and linear probing for - * cache efficient access. + * {@link Map} implementation specialised for int keys using open addressing and + * linear probing for cache efficient access. * * @param type of values stored in the {@link Map} */ -public class Int2ObjectHashMap implements Map, Serializable { +public class Int2ObjectHashMap + implements Map, Serializable { static final int MIN_CAPACITY = 8; private final float loadFactor; @@ -56,7 +73,9 @@ public Int2ObjectHashMap() { this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, true); } - public Int2ObjectHashMap(final int initialCapacity, final float loadFactor) { + public Int2ObjectHashMap( + final int initialCapacity, + final float loadFactor) { this(initialCapacity, loadFactor, true); } @@ -68,14 +87,18 @@ public Int2ObjectHashMap(final int initialCapacity, final float loadFactor) { * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. */ public Int2ObjectHashMap( - final int initialCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { + final int initialCapacity, + final float loadFactor, + final boolean shouldAvoidAllocation) { validateLoadFactor(loadFactor); this.loadFactor = loadFactor; this.shouldAvoidAllocation = shouldAvoidAllocation; - /* */ final int capacity = findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity)); - /* */ resizeThreshold = (int) (capacity * loadFactor); + /* */ + final int capacity = findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity)); + /* */ + resizeThreshold = (int) (capacity * loadFactor); keys = new int[capacity]; values = new Object[capacity]; @@ -115,8 +138,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. This is a function of the - * current capacity and load factor. + * Get the actual threshold which when reached the map will resize. + * This is a function of the current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -124,17 +147,23 @@ public int resizeThreshold() { return resizeThreshold; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return size; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return 0 == size; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean containsKey(final Object key) { return containsKey(((Integer) key).intValue()); } @@ -162,7 +191,9 @@ public boolean containsKey(final int key) { return found; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean containsValue(final Object value) { boolean found = false; final Object val = mapNullValue(value); @@ -178,7 +209,9 @@ public boolean containsValue(final Object value) { return found; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public V get(final Object key) { return get(((Integer) key).intValue()); } @@ -210,7 +243,9 @@ protected V getMapped(final int key) { return (V) value; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public V put(final Integer key, final V value) { return put(key.intValue(), value); } @@ -254,7 +289,9 @@ public V put(final int key, final V value) { return unmapNullValue(oldValue); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public V remove(final Object key) { return remove(((Integer) key).intValue()); } @@ -285,7 +322,9 @@ public V remove(final int key) { return unmapNullValue(value); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { if (size > 0) { Arrays.fill(values, null); @@ -294,22 +333,26 @@ public void clear() { } /** - * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current - * size and giving consideration to the load factor. + * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current size + * and giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void putAll(final Map map) { for (final Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public KeySet keySet() { if (null == keySet) { keySet = new KeySet(); @@ -318,7 +361,9 @@ public KeySet keySet() { return keySet; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public ValueCollection values() { if (null == valueCollection) { valueCollection = new ValueCollection(); @@ -327,7 +372,9 @@ public ValueCollection values() { return valueCollection; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public EntrySet entrySet() { if (null == entrySet) { entrySet = new EntrySet(); @@ -336,7 +383,9 @@ public EntrySet entrySet() { return entrySet; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public String toString() { if (isEmpty()) { return "{}"; @@ -348,8 +397,7 @@ public String toString() { final StringBuilder sb = new StringBuilder().append('{'); while (true) { entryIterator.next(); - sb.append(entryIterator.getIntKey()) - .append('=') + sb.append(entryIterator.getIntKey()).append('=') .append(unmapNullValue(entryIterator.getValue())); if (!entryIterator.hasNext()) { return sb.append('}').toString(); @@ -358,7 +406,9 @@ public String toString() { } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean equals(final Object o) { if (this == o) { return true; @@ -387,7 +437,9 @@ public boolean equals(final Object o) { return true; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int hashCode() { int result = 0; @@ -415,8 +467,8 @@ protected V unmapNullValue(final Object value) { * * @param key key with which the specified value is associated * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or {@code null} if there was no - * mapping for the key. + * @return the previous value associated with the specified key, or + * {@code null} if there was no mapping for the key. */ public V replace(final int key, final V value) { V curValue = get(key); @@ -457,7 +509,8 @@ private void increaseCapacity() { private void rehash(final int newCapacity) { final int mask = newCapacity - 1; - /* */ resizeThreshold = (int) (newCapacity * loadFactor); + /* */ + resizeThreshold = (int) (newCapacity * loadFactor); final int[] tempKeys = new int[newCapacity]; final Object[] tempValues = new Object[newCapacity]; @@ -492,8 +545,8 @@ private void compactChain(int deleteIndex) { final int hash = Hashing.hash(keys[index], mask); - if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) - || (hash <= deleteIndex && deleteIndex <= index)) { + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || + (hash <= deleteIndex && deleteIndex <= index)) { keys[deleteIndex] = keys[index]; values[deleteIndex] = values[index]; @@ -507,11 +560,15 @@ private void compactChain(int deleteIndex) { // Sets and Collections /////////////////////////////////////////////////////////////////////////////////////////////// - /** Set of keys which supports optionally cached iterators to avoid allocation. */ + /** + * Set of keys which supports optionally cached iterators to avoid allocation. + */ public final class KeySet extends AbstractSet implements Serializable { private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public KeyIterator iterator() { KeyIterator keyIterator = this.keyIterator; if (null == keyIterator) { @@ -547,11 +604,15 @@ public void clear() { } } - /** Collection of values which supports optionally cached iterators to avoid allocation. */ + /** + * Collection of values which supports optionally cached iterators to avoid allocation. + */ public final class ValueCollection extends AbstractCollection implements Serializable { private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public ValueIterator iterator() { ValueIterator valueIterator = this.valueIterator; if (null == valueIterator) { @@ -575,11 +636,15 @@ public void clear() { } } - /** Set of entries which supports access via an optionally cached iterator to avoid allocation. */ + /** + * Set of entries which supports access via an optionally cached iterator to avoid allocation. + */ public final class EntrySet extends AbstractSet> implements Serializable { private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public EntryIterator iterator() { EntryIterator entryIterator = this.entryIterator; if (null == entryIterator) { @@ -598,7 +663,9 @@ public void clear() { Int2ObjectHashMap.this.clear(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object o) { final Entry entry = (Entry) o; final int key = (Integer) entry.getKey(); @@ -687,7 +754,9 @@ final void reset() { } } - /** Iterator over values. */ + /** + * Iterator over values. + */ public class ValueIterator extends AbstractIterator { public V next() { findNext(); @@ -696,7 +765,9 @@ public V next() { } } - /** Iterator over keys which supports access to unboxed keys. */ + /** + * Iterator over keys which supports access to unboxed keys. + */ public class KeyIterator extends AbstractIterator { public Integer next() { return nextInt(); @@ -709,8 +780,11 @@ public int nextInt() { } } - /** Iterator over entries which supports access to unboxed keys and values. */ - public class EntryIterator extends AbstractIterator> + /** + * Iterator over entries which supports access to unboxed keys and values. + */ + public class EntryIterator + extends AbstractIterator> implements Entry { public Entry next() { findNext(); @@ -749,8 +823,8 @@ public boolean equals(final Object o) { final Entry e = (Entry) o; - return (e.getKey() != null && e.getKey().equals(k)) - && ((e.getValue() == null && v == null) || e.getValue().equals(v)); + return (e.getKey() != null && e.getKey().equals(k)) && + ((e.getValue() == null && v == null) || e.getValue().equals(v)); } public String toString() { diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java index cde2b72f8..0fb9e7fd0 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java @@ -16,11 +16,29 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; -/** This is an (int, int) primitive specialisation of a BiConsumer */ +/** + * This is an (int, int) primitive specialisation of a BiConsumer + */ @FunctionalInterface -public interface IntIntConsumer { +public interface +IntIntConsumer { /** * Accept two values that comes as a tuple of ints. * diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java index aaf221fd6..d1d7d7ce9 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java @@ -16,10 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; - -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; +/* + * Copyright 2014-2020 Real Logic Limited. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; import java.io.Serializable; import java.util.AbstractCollection; @@ -30,7 +42,12 @@ import java.util.NoSuchElementException; import java.util.Objects; -/** A open addressing with linear probing hash map specialised for primitive key and value pairs. */ +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; + +/** + * A open addressing with linear probing hash map specialised for primitive key and value pairs. + */ public class Long2LongHashMap implements Map, Serializable { static final int MIN_CAPACITY = 8; @@ -50,7 +67,9 @@ public Long2LongHashMap(final long missingValue) { } public Long2LongHashMap( - final int initialCapacity, final float loadFactor, final long missingValue) { + final int initialCapacity, + final float loadFactor, + final long missingValue) { this(initialCapacity, loadFactor, missingValue, true); } @@ -102,8 +121,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. This is a function of the - * current capacity and load factor. + * Get the actual threshold which when reached the map will resize. + * This is a function of the current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -111,12 +130,16 @@ public int resizeThreshold() { return resizeThreshold; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return size; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return size == 0; } @@ -216,9 +239,9 @@ private void rehash(final int newCapacity) { /** * Primitive specialised forEach implementation. - * - *

    NB: Renamed from forEach to avoid overloading on parameter types of lambda expression, which - * doesn't play well with type inference in lambda expressions. + *

    + * NB: Renamed from forEach to avoid overloading on parameter types of lambda + * expression, which doesn't play well with type inference in lambda expressions. * * @param consumer a callback called for each key/value pair in the map. */ @@ -230,8 +253,8 @@ public void longForEach(final LongLongConsumer consumer) { for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { if (entries[keyIndex + 1] != missingValue) // lgtm [java/index-out-of-bounds] { - consumer.accept( - entries[keyIndex], entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] + consumer.accept(entries[keyIndex], + entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] } } } @@ -269,7 +292,9 @@ public boolean containsValue(final long value) { return found; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { if (size > 0) { Arrays.fill(entries, missingValue); @@ -278,8 +303,8 @@ public void clear() { } /** - * Compact the backing arrays by rehashing with a capacity just larger than current size and - * giving consideration to the load factor. + * Compact the backing arrays by rehashing with a capacity just larger than current size + * and giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); @@ -288,34 +313,46 @@ public void compact() { // ---------------- Boxed Versions Below ---------------- - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public Long get(final Object key) { return valOrNull(get((long) key)); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public Long put(final Long key, final Long value) { return valOrNull(put((long) key, (long) value)); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean containsKey(final Object key) { return containsKey((long) key); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean containsValue(final Object value) { return containsValue((long) value); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void putAll(final Map map) { for (final Map.Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public KeySet keySet() { if (null == keySet) { keySet = new KeySet(); @@ -324,7 +361,9 @@ public KeySet keySet() { return keySet; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public ValueCollection values() { if (null == values) { values = new ValueCollection(); @@ -333,7 +372,9 @@ public ValueCollection values() { return values; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public EntrySet entrySet() { if (null == entrySet) { entrySet = new EntrySet(); @@ -342,7 +383,9 @@ public EntrySet entrySet() { return entrySet; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public Long remove(final Object key) { return valOrNull(remove((long) key)); } @@ -386,8 +429,8 @@ private void compactChain(int deleteKeyIndex) { final int hash = Hashing.evenHash(entries[keyIndex], mask); - if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) - || (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { + if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) || + (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { entries[deleteKeyIndex] = entries[keyIndex]; entries[deleteKeyIndex + 1] = entries[keyIndex + 1]; @@ -398,8 +441,7 @@ private void compactChain(int deleteKeyIndex) { } /** - * Get the minimum value stored in the map. If the map is empty then it will return {@link - * #missingValue()} + * Get the minimum value stored in the map. If the map is empty then it will return {@link #missingValue()} * * @return the minimum value stored in the map. */ @@ -421,8 +463,7 @@ public long minValue() { } /** - * Get the maximum value stored in the map. If the map is empty then it will return {@link - * #missingValue()} + * Get the maximum value stored in the map. If the map is empty then it will return {@link #missingValue()} * * @return the maximum value stored in the map. */ @@ -443,7 +484,9 @@ public long maxValue() { return max; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public String toString() { if (isEmpty()) { return "{}"; @@ -468,8 +511,8 @@ public String toString() { * * @param key key with which the specified value is associated * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or {@link #missingValue()} if - * there was no mapping for the key. + * @return the previous value associated with the specified key, or + * {@link #missingValue()} if there was no mapping for the key. */ public long replace(final long key, final long value) { long currentValue = get(key); @@ -499,7 +542,9 @@ public boolean replace(final long key, final long oldValue, final long newValue) return true; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean equals(final Object o) { if (this == o) { return true; @@ -511,6 +556,7 @@ public boolean equals(final Object o) { final Map that = (Map) o; return size == that.size() && entrySet().equals(that.entrySet()); + } public int hashCode() { @@ -616,7 +662,9 @@ public void remove() { } } - /** Iterator over keys which supports access to unboxed keys. */ + /** + * Iterator over keys which supports access to unboxed keys. + */ public final class KeyIterator extends AbstractIterator implements Iterator { public Long next() { return nextValue(); @@ -628,7 +676,9 @@ public long nextValue() { } } - /** Iterator over values which supports access to unboxed values. */ + /** + * Iterator over values which supports access to unboxed values. + */ public final class ValueIterator extends AbstractIterator implements Iterator { public Long next() { return nextValue(); @@ -640,8 +690,11 @@ public long nextValue() { } } - /** Iterator over entries which supports access to unboxed keys and values. */ - public final class EntryIterator extends AbstractIterator + /** + * Iterator over entries which supports access to unboxed keys and values. + */ + public final class EntryIterator + extends AbstractIterator implements Iterator>, Entry { public Long getKey() { return getLongKey(); @@ -716,8 +769,8 @@ public boolean equals(final Object o) { final Map.Entry e = (Entry) o; - return (e.getKey() != null && e.getValue() != null) - && (e.getKey().equals(k) && e.getValue().equals(v)); + return (e.getKey() != null && e.getValue() != null) && + (e.getKey().equals(k) && e.getValue().equals(v)); } public String toString() { @@ -726,12 +779,16 @@ public String toString() { }; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int hashCode() { return Hashing.hashCode(getLongKey()) ^ Hashing.hashCode(getLongValue()); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean equals(final Object o) { if (this == o) { return true; @@ -747,11 +804,15 @@ public boolean equals(final Object o) { } } - /** Set of keys which supports optional cached iterators to avoid allocation. */ + /** + * Set of keys which supports optional cached iterators to avoid allocation. + */ public final class KeySet extends AbstractSet implements Serializable { private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public KeyIterator iterator() { KeyIterator keyIterator = this.keyIterator; if (null == keyIterator) { @@ -763,22 +824,30 @@ public KeyIterator iterator() { return keyIterator; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return Long2LongHashMap.this.size(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return Long2LongHashMap.this.isEmpty(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { Long2LongHashMap.this.clear(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object o) { return contains((long) o); } @@ -788,11 +857,15 @@ public boolean contains(final long key) { } } - /** Collection of values which supports optionally cached iterators to avoid allocation. */ + /** + * Collection of values which supports optionally cached iterators to avoid allocation. + */ public final class ValueCollection extends AbstractCollection { private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public ValueIterator iterator() { ValueIterator valueIterator = this.valueIterator; if (null == valueIterator) { @@ -804,12 +877,16 @@ public ValueIterator iterator() { return valueIterator; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return Long2LongHashMap.this.size(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object o) { return contains((long) o); } @@ -819,11 +896,15 @@ public boolean contains(final long key) { } } - /** Set of entries which supports optionally cached iterators to avoid allocation. */ + /** + * Set of entries which supports optionally cached iterators to avoid allocation. + */ public final class EntrySet extends AbstractSet> implements Serializable { private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public EntryIterator iterator() { EntryIterator entryIterator = this.entryIterator; if (null == entryIterator) { @@ -835,22 +916,30 @@ public EntryIterator iterator() { return entryIterator; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return Long2LongHashMap.this.size(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return Long2LongHashMap.this.isEmpty(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { Long2LongHashMap.this.clear(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object o) { if (!(o instanceof Entry)) { return false; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java index 809f08566..01ce2b507 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java @@ -16,11 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; - -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; -import static java.util.Objects.requireNonNull; +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; import java.io.Serializable; import java.util.AbstractCollection; @@ -31,13 +42,19 @@ import java.util.NoSuchElementException; import java.util.Objects; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; +import static java.util.Objects.requireNonNull; + + /** - * {@link Map} implementation specialised for long keys using open addressing and linear probing for - * cache efficient access. + * {@link Map} implementation specialised for long keys using open addressing and + * linear probing for cache efficient access. * * @param type of values stored in the {@link Map} */ -public class Long2ObjectHashMap implements Map, Serializable { +public class Long2ObjectHashMap + implements Map, Serializable { static final int MIN_CAPACITY = 8; private final float loadFactor; @@ -56,7 +73,9 @@ public Long2ObjectHashMap() { this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, true); } - public Long2ObjectHashMap(final int initialCapacity, final float loadFactor) { + public Long2ObjectHashMap( + final int initialCapacity, + final float loadFactor) { this(initialCapacity, loadFactor, true); } @@ -68,14 +87,18 @@ public Long2ObjectHashMap(final int initialCapacity, final float loadFactor) { * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. */ public Long2ObjectHashMap( - final int initialCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { + final int initialCapacity, + final float loadFactor, + final boolean shouldAvoidAllocation) { validateLoadFactor(loadFactor); this.loadFactor = loadFactor; this.shouldAvoidAllocation = shouldAvoidAllocation; - /* */ final int capacity = findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity)); - /* */ resizeThreshold = (int) (capacity * loadFactor); + /* */ + final int capacity = findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, initialCapacity)); + /* */ + resizeThreshold = (int) (capacity * loadFactor); keys = new long[capacity]; values = new Object[capacity]; @@ -115,8 +138,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. This is a function of the - * current capacity and load factor. + * Get the actual threshold which when reached the map will resize. + * This is a function of the current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -124,17 +147,23 @@ public int resizeThreshold() { return resizeThreshold; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return size; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return 0 == size; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean containsKey(final Object key) { return containsKey(((Long) key).longValue()); } @@ -162,7 +191,9 @@ public boolean containsKey(final long key) { return found; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean containsValue(final Object value) { boolean found = false; final Object val = mapNullValue(value); @@ -178,7 +209,9 @@ public boolean containsValue(final Object value) { return found; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public V get(final Object key) { return get(((Long) key).longValue()); } @@ -210,7 +243,9 @@ protected V getMapped(final long key) { return (V) value; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public V put(final Long key, final V value) { return put(key.longValue(), value); } @@ -254,7 +289,9 @@ public V put(final long key, final V value) { return unmapNullValue(oldValue); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public V remove(final Object key) { return remove(((Long) key).longValue()); } @@ -285,7 +322,9 @@ public V remove(final long key) { return unmapNullValue(value); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { if (size > 0) { Arrays.fill(values, null); @@ -294,22 +333,26 @@ public void clear() { } /** - * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current - * size and giving consideration to the load factor. + * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current size + * and giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void putAll(final Map map) { for (final Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public KeySet keySet() { if (null == keySet) { keySet = new KeySet(); @@ -318,7 +361,9 @@ public KeySet keySet() { return keySet; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public ValueCollection values() { if (null == valueCollection) { valueCollection = new ValueCollection(); @@ -327,7 +372,9 @@ public ValueCollection values() { return valueCollection; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public EntrySet entrySet() { if (null == entrySet) { entrySet = new EntrySet(); @@ -336,7 +383,9 @@ public EntrySet entrySet() { return entrySet; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public String toString() { if (isEmpty()) { return "{}"; @@ -348,8 +397,7 @@ public String toString() { final StringBuilder sb = new StringBuilder().append('{'); while (true) { entryIterator.next(); - sb.append(entryIterator.getLongKey()) - .append('=') + sb.append(entryIterator.getLongKey()).append('=') .append(unmapNullValue(entryIterator.getValue())); if (!entryIterator.hasNext()) { return sb.append('}').toString(); @@ -358,7 +406,9 @@ public String toString() { } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean equals(final Object o) { if (this == o) { return true; @@ -387,7 +437,9 @@ public boolean equals(final Object o) { return true; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int hashCode() { int result = 0; @@ -415,8 +467,8 @@ protected V unmapNullValue(final Object value) { * * @param key key with which the specified value is associated * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or {@code null} if there was no - * mapping for the key. + * @return the previous value associated with the specified key, or + * {@code null} if there was no mapping for the key. */ public V replace(final long key, final V value) { V curValue = get(key); @@ -457,7 +509,8 @@ private void increaseCapacity() { private void rehash(final int newCapacity) { final int mask = newCapacity - 1; - /* */ resizeThreshold = (int) (newCapacity * loadFactor); + /* */ + resizeThreshold = (int) (newCapacity * loadFactor); final long[] tempKeys = new long[newCapacity]; final Object[] tempValues = new Object[newCapacity]; @@ -492,8 +545,8 @@ private void compactChain(int deleteIndex) { final int hash = Hashing.hash(keys[index], mask); - if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) - || (hash <= deleteIndex && deleteIndex <= index)) { + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || + (hash <= deleteIndex && deleteIndex <= index)) { keys[deleteIndex] = keys[index]; values[deleteIndex] = values[index]; @@ -507,11 +560,15 @@ private void compactChain(int deleteIndex) { // Sets and Collections /////////////////////////////////////////////////////////////////////////////////////////////// - /** Set of keys which supports optionally cached iterators to avoid allocation. */ + /** + * Set of keys which supports optionally cached iterators to avoid allocation. + */ public final class KeySet extends AbstractSet implements Serializable { private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public KeyIterator iterator() { KeyIterator keyIterator = this.keyIterator; if (null == keyIterator) { @@ -547,11 +604,15 @@ public void clear() { } } - /** Collection of values which supports optionally cached iterators to avoid allocation. */ + /** + * Collection of values which supports optionally cached iterators to avoid allocation. + */ public final class ValueCollection extends AbstractCollection implements Serializable { private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public ValueIterator iterator() { ValueIterator valueIterator = this.valueIterator; if (null == valueIterator) { @@ -575,11 +636,15 @@ public void clear() { } } - /** Set of entries which supports access via an optionally cached iterator to avoid allocation. */ + /** + * Set of entries which supports access via an optionally cached iterator to avoid allocation. + */ public final class EntrySet extends AbstractSet> implements Serializable { private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public EntryIterator iterator() { EntryIterator entryIterator = this.entryIterator; if (null == entryIterator) { @@ -598,7 +663,9 @@ public void clear() { Long2ObjectHashMap.this.clear(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object o) { final Entry entry = (Entry) o; final long key = (Long) entry.getKey(); @@ -687,7 +754,9 @@ final void reset() { } } - /** Iterator over values. */ + /** + * Iterator over values. + */ public class ValueIterator extends AbstractIterator { public V next() { findNext(); @@ -696,7 +765,9 @@ public V next() { } } - /** Iterator over keys which supports access to unboxed keys. */ + /** + * Iterator over keys which supports access to unboxed keys. + */ public class KeyIterator extends AbstractIterator { public Long next() { return nextLong(); @@ -709,8 +780,12 @@ public long nextLong() { } } - /** Iterator over entries which supports access to unboxed keys and values. */ - public class EntryIterator extends AbstractIterator> implements Entry { + /** + * Iterator over entries which supports access to unboxed keys and values. + */ + public class EntryIterator + extends AbstractIterator> + implements Entry { public Entry next() { findNext(); if (shouldAvoidAllocation) { @@ -748,8 +823,8 @@ public boolean equals(final Object o) { final Entry e = (Entry) o; - return (e.getKey() != null && e.getKey().equals(k)) - && ((e.getValue() == null && v == null) || e.getValue().equals(v)); + return (e.getKey() != null && e.getKey().equals(k)) && + ((e.getValue() == null && v == null) || e.getValue().equals(v)); } public String toString() { diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java index da9c383fe..8a7ae4733 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java @@ -16,10 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; +/* + * Copyright 2014-2020 Real Logic Limited. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.agent.profiler.collections.CollectionUtil.validateLoadFactor; import java.io.Serializable; import java.lang.reflect.Array; @@ -30,25 +43,28 @@ import java.util.NoSuchElementException; import java.util.Set; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; + /** - * Open-addressing with linear-probing expandable hash set. Allocation free in steady state use when - * expanded. - * - *

    By storing elements as long primitives this significantly reduces memory consumption compared - * with Java's builtin HashSet<Long>. It implements Set<Long> - * for convenience, but calling functionality via those methods can add boxing overhead to your - * usage. - * - *

    This class is not Threadsafe. - * - *

    This HashSet caches its iterator object by default, so nested iteration is not supported. You - * can override this behaviour at construction by indicating that the iterator should not be cached. + * Open-addressing with linear-probing expandable hash set. Allocation free in steady state use when expanded. + *

    + * By storing elements as long primitives this significantly reduces memory consumption compared with Java's builtin + * HashSet<Long>. It implements Set<Long> for convenience, but calling + * functionality via those methods can add boxing overhead to your usage. + *

    + * This class is not Threadsafe. + *

    + * This HashSet caches its iterator object by default, so nested iteration is not supported. You can override this + * behaviour at construction by indicating that the iterator should not be cached. * * @see LongIterator * @see Set */ public class LongHashSet extends AbstractSet implements Serializable { - /** The initial capacity used when none is specified in the constructor. */ + /** + * The initial capacity used when none is specified in the constructor. + */ public static final int DEFAULT_INITIAL_CAPACITY = 8; static final long MISSING_VALUE = -1; @@ -64,51 +80,54 @@ public class LongHashSet extends AbstractSet implements Serializable { private LongIterator iterator; /** - * Construct a hash set with {@link #DEFAULT_INITIAL_CAPACITY}, {@link - * Hashing#DEFAULT_LOAD_FACTOR}, and iterator caching support. + * Construct a hash set with {@link #DEFAULT_INITIAL_CAPACITY}, {@link Hashing#DEFAULT_LOAD_FACTOR}, + * and iterator caching support. */ public LongHashSet() { this(DEFAULT_INITIAL_CAPACITY); } /** - * Construct a hash set with a proposed capacity, {@link Hashing#DEFAULT_LOAD_FACTOR}, and - * iterator caching support. + * Construct a hash set with a proposed capacity, {@link Hashing#DEFAULT_LOAD_FACTOR}, + * and iterator caching support. * * @param proposedCapacity for the initial capacity of the set. */ - public LongHashSet(final int proposedCapacity) { + public LongHashSet( + final int proposedCapacity) { this(proposedCapacity, Hashing.DEFAULT_LOAD_FACTOR, true); } /** - * Construct a hash set with a proposed initial capacity, load factor, and iterator caching - * support. + * Construct a hash set with a proposed initial capacity, load factor, and iterator caching support. * * @param proposedCapacity for the initial capacity of the set. * @param loadFactor to be used for resizing. */ - public LongHashSet(final int proposedCapacity, final float loadFactor) { + public LongHashSet( + final int proposedCapacity, + final float loadFactor) { this(proposedCapacity, loadFactor, true); } /** - * Construct a hash set with a proposed initial capacity, load factor, and indicated iterator - * caching support. + * Construct a hash set with a proposed initial capacity, load factor, and indicated iterator caching support. * * @param proposedCapacity for the initial capacity of the set. * @param loadFactor to be used for resizing. * @param shouldAvoidAllocation should the iterator be cached to avoid further allocation. */ public LongHashSet( - final int proposedCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { + final int proposedCapacity, + final float loadFactor, + final boolean shouldAvoidAllocation) { validateLoadFactor(loadFactor); this.shouldAvoidAllocation = shouldAvoidAllocation; this.loadFactor = loadFactor; sizeOfArrayValues = 0; - final int capacity = - findNextPositivePowerOfTwo(Math.max(DEFAULT_INITIAL_CAPACITY, proposedCapacity)); + final int capacity = findNextPositivePowerOfTwo( + Math.max(DEFAULT_INITIAL_CAPACITY, proposedCapacity)); resizeThreshold = (int) (capacity * loadFactor); // @DoNotSub values = new long[capacity]; Arrays.fill(values, MISSING_VALUE); @@ -133,8 +152,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. This is a function of the - * current capacity and load factor. + * Get the actual threshold which when reached the map will resize. + * This is a function of the current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -142,7 +161,9 @@ public int resizeThreshold() { return resizeThreshold; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean add(final Long value) { return add(value.longValue()); } @@ -214,7 +235,9 @@ private void rehash(final int newCapacity) { values = tempValues; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean remove(final Object value) { return value instanceof Long && remove(((Long) value).longValue()); } @@ -268,8 +291,8 @@ void compactChain(int deleteIndex) { final int hash = Hashing.hash(values[index], mask); - if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) - || (hash <= deleteIndex && deleteIndex <= index)) { + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || + (hash <= deleteIndex && deleteIndex <= index)) { values[deleteIndex] = values[index]; values[index] = MISSING_VALUE; @@ -279,15 +302,17 @@ void compactChain(int deleteIndex) { } /** - * Compact the backing arrays by rehashing with a capacity just larger than current size and - * giving consideration to the load factor. + * Compact the backing arrays by rehashing with a capacity just larger than current size + * and giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0 / loadFactor)); rehash(findNextPositivePowerOfTwo(Math.max(DEFAULT_INITIAL_CAPACITY, idealCapacity))); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean contains(final Object value) { return value instanceof Long && contains(((Long) value).longValue()); } @@ -319,17 +344,23 @@ public boolean contains(final long value) { return false; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int size() { return sizeOfArrayValues + (containsMissingValue ? 1 : 0); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean isEmpty() { return size() == 0; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public void clear() { if (size() > 0) { Arrays.fill(values, MISSING_VALUE); @@ -338,7 +369,9 @@ public void clear() { } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean addAll(final Collection coll) { boolean added = false; @@ -390,8 +423,8 @@ public boolean containsAll(final LongHashSet other) { /** * Fast Path set difference for comparison with another LongHashSet. - * - *

    Note: garbage free in the identical case, allocates otherwise. + *

    + * Note: garbage free in the identical case, allocates otherwise. * * @param other the other set to subtract * @return null if identical, otherwise the set of differences @@ -420,7 +453,9 @@ public LongHashSet difference(final LongHashSet other) { return difference; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean removeAll(final Collection coll) { boolean removed = false; @@ -432,8 +467,8 @@ public boolean removeAll(final Collection coll) { } /** - * Alias for {@link #removeAll(Collection)} for the specialized case when removing another - * LongHashSet, avoids boxing and allocations + * Alias for {@link #removeAll(Collection)} for the specialized case when removing another LongHashSet, + * avoids boxing and allocations * * @param coll containing the values to be removed. * @return {@code true} if this set changed as a result of the call @@ -454,7 +489,9 @@ public boolean removeAll(final LongHashSet coll) { return acc; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public LongIterator iterator() { LongIterator iterator = this.iterator; if (null == iterator) { @@ -477,7 +514,9 @@ public void copy(final LongHashSet that) { this.containsMissingValue = that.containsMissingValue; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public String toString() { final StringBuilder sb = new StringBuilder(); sb.append('{'); @@ -501,7 +540,9 @@ public String toString() { return sb.toString(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ @SuppressWarnings("unchecked") public T[] toArray(final T[] a) { final Class componentType = a.getClass().getComponentType(); @@ -516,7 +557,9 @@ public T[] toArray(final T[] a) { return arrayCopy; } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public Object[] toArray() { final Object[] arrayCopy = new Object[size()]; copyValues(arrayCopy); @@ -538,7 +581,9 @@ private void copyValues(final Object[] arrayCopy) { } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public boolean equals(final Object other) { if (other == this) { return true; @@ -547,9 +592,9 @@ public boolean equals(final Object other) { if (other instanceof LongHashSet) { final LongHashSet otherSet = (LongHashSet) other; - return otherSet.containsMissingValue == containsMissingValue - && otherSet.sizeOfArrayValues == sizeOfArrayValues - && containsAll(otherSet); + return otherSet.containsMissingValue == containsMissingValue && + otherSet.sizeOfArrayValues == sizeOfArrayValues && + containsAll(otherSet); } if (!(other instanceof Set)) { @@ -568,7 +613,9 @@ public boolean equals(final Object other) { } } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ public int hashCode() { int hashCode = 0; for (final long value : values) { @@ -584,7 +631,9 @@ public int hashCode() { return hashCode; } - /** Iterator which supports unboxed access to values. */ + /** + * Iterator which supports unboxed access to values. + */ public final class LongIterator implements Iterator, Serializable { private int remaining; private int positionCounter; @@ -682,8 +731,10 @@ private void findNext() { throw new NoSuchElementException(); } - private int position(final long[] values) { + private int position( + final long[] values) { return positionCounter & (values.length - 1); } } } + diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java new file mode 100644 index 000000000..cdf334e9b --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java @@ -0,0 +1,123 @@ +package co.elastic.apm.otel.profiler.collections; + + +import java.util.Arrays; + +public class LongList { + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + private static final int DEFAULT_CAPACITY = 16; + private long[] longs; + private int size; + + public LongList() { + this(DEFAULT_CAPACITY); + } + + public LongList(int initialCapacity) { + longs = new long[initialCapacity]; + } + + public static LongList of(long... values) { + LongList list = new LongList(values.length); + for (long value : values) { + list.add(value); + } + return list; + } + + public void add(long l) { + ensureCapacity(size + 1); + longs[size++] = l; + } + + public void addAll(LongList other) { + ensureCapacity(size + other.size); + System.arraycopy(other.longs, 0, longs, size, other.size); + size += other.size; + } + + private void ensureCapacity(long minCapacity) { + if (longs.length < minCapacity) { + longs = Arrays.copyOf(longs, newCapacity(minCapacity, longs.length)); + } + } + + static int newCapacity(long minCapacity, long oldCapacity) { + long growBy50Percent = oldCapacity + (oldCapacity >> 1); + if (minCapacity <= growBy50Percent) { + return (int) growBy50Percent; + } else if (minCapacity <= MAX_ARRAY_SIZE) { + return (int) minCapacity; + } else { + throw new OutOfMemoryError(); + } + } + + public int getSize() { + return size; + } + + public long get(int i) { + if (i >= size) { + throw new IndexOutOfBoundsException(); + } + return longs[i]; + } + + public boolean contains(long l) { + for (int i = 0; i < size; i++) { + if (longs[i] == l) { + return true; + } + } + return false; + } + + public boolean remove(long l) { + for (int i = size - 1; i >= 0; i--) { + if (longs[i] == l) { + remove(i); + return true; + } + } + return false; + } + + public long remove(int i) { + long previousValue = get(i); + size--; + if (size > i) { + System.arraycopy(longs, i + 1, longs, i, size - i); + } + longs[size] = 0; + return previousValue; + } + + public void clear() { + Arrays.fill(longs, 0); + size = 0; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (int i = 0; i < size; i++) { + if (i > 0) { + sb.append(','); + } + sb.append(longs[i]); + } + sb.append(']'); + return sb.toString(); + } + + public long[] toArray() { + return Arrays.copyOfRange(longs, 0, size); + } + + public boolean isEmpty() { + return size == 0; + } +} + diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java index 503ac6af8..f3097236b 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java @@ -16,11 +16,29 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.collections; +/* + * Copyright 2014-2020 Real Logic Limited. + * + * 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 + * + * https://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 co.elastic.apm.otel.profiler.collections; -/** This is an (long, long) primitive specialisation of a BiConsumer */ +/** + * This is an (long, long) primitive specialisation of a BiConsumer + */ @FunctionalInterface -public interface LongLongConsumer { +public interface +LongLongConsumer { /** * Accept two values that comes as a tuple of longs. * diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java index aa711216b..4f7ac61bb 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java @@ -25,4 +25,4 @@ * Java 7. That's why the relevant classes are copied over and methods referencing Java 8 types are * removed. */ -package co.elastic.apm.agent.profiler.collections; +package co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java new file mode 100644 index 000000000..c90e9dd84 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java @@ -0,0 +1,373 @@ +package co.elastic.apm.otel.profiler.config; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +/** + * This matcher is used in for example to disable tracing for certain URLs. + * The advantage of this class compared to alternatives is that {@linkplain #matches(CharSequence) matching} strings is completely allocation free. + *

    + * The wildcard matcher supports the {@code *} wildcard which matches zero or more characters. + * By default, matches are a case insensitive. + * Single character wildcards like {@code f?o} are not supported. + *

    + *

    + * The syntax should be very familiar to any developer. + * The most common use cases are ignoring URLs paths starting with a specific string like {@code /resources/*} or /heartbeat/* + * and ignoring URLs by file ending like {@code *.js}. + * It also allows to have a single configuration option which, + * depending on the input string, + * allows for prefix, postfix and infix matching. + * This implementation is also very fast, + * as it just resorts to {@link String#startsWith(String)}, + * {@link String#endsWith(String)} and {@link String#contains(CharSequence)}. + *

    + */ +// don't use for-each as it allocates memory by instantiating an iterator +@SuppressWarnings("ForLoopReplaceableByForEach") +public abstract class WildcardMatcher { + public static final String DOCUMENTATION = + "This option supports the wildcard `*`, which matches zero or more characters.\n" + + "Examples: `/foo/*/bar/*/baz*`, `*foo*`.\n" + + "Matching is case insensitive by default.\n" + + "Prepending an element with `(?-i)` makes the matching case sensitive."; + private static final String CASE_INSENSITIVE_PREFIX = "(?i)"; + private static final String CASE_SENSITIVE_PREFIX = "(?-i)"; + private static final String WILDCARD = "*"; + private static final WildcardMatcher MATCH_ALL = valueOf(WILDCARD); + private static final List MATCH_ALL_LIST = Collections.singletonList(MATCH_ALL); + + public static WildcardMatcher caseSensitiveMatcher(String matcher) { + return valueOf(CASE_SENSITIVE_PREFIX + matcher); + } + + public static WildcardMatcher matchAll() { + return MATCH_ALL; + } + + public static List matchAllList() { + return MATCH_ALL_LIST; + } + + /** + * Constructs a new {@link WildcardMatcher} via a wildcard string. + *

    + * It supports the {@code *} wildcard which matches zero or more characters. + *

    + *

    + * By default, matches are a case insensitive. + * Prepend {@code (?-i)} to your pattern to make it case sensitive. + * Example: {@code (?-i)foo*} matches the string {@code foobar} but does not match {@code FOOBAR}. + *

    + *

    + * It does NOT support single character wildcards like {@code f?o} + *

    + * + * @param wildcardString The wildcard string. + * @return The {@link WildcardMatcher} + */ + public static WildcardMatcher valueOf(final String wildcardString) { + String matcher = wildcardString; + boolean ignoreCase = true; + if (matcher.startsWith(CASE_SENSITIVE_PREFIX)) { + ignoreCase = false; + matcher = matcher.substring(CASE_SENSITIVE_PREFIX.length()); + } else if (matcher.startsWith(CASE_INSENSITIVE_PREFIX)) { + matcher = matcher.substring(CASE_INSENSITIVE_PREFIX.length()); + } + + String[] split = matcher.split("\\*"); + if (split.length == 1) { + return new SimpleWildcardMatcher(split[0], matcher.startsWith(WILDCARD), + matcher.endsWith(WILDCARD), ignoreCase); + } + + List matchers = new ArrayList<>(split.length); + for (int i = 0; i < split.length; i++) { + boolean isFirst = i == 0; + boolean isLast = i == split.length - 1; + matchers.add(new SimpleWildcardMatcher(split[i], + !isFirst || matcher.startsWith(WILDCARD), + !isLast || matcher.endsWith(WILDCARD), + ignoreCase)); + } + return new CompoundWildcardMatcher(wildcardString, matcher, matchers); + } + + /** + * Returns {@code true}, if any of the matchers match the provided string. + * + * @param matchers the matchers which should be used to match the provided string + * @param s the string to match against + * @return {@code true}, if any of the matchers match the provided string + */ + public static boolean isAnyMatch(List matchers, @Nullable CharSequence s) { + return anyMatch(matchers, s) != null; + } + + /** + * Returns {@code true}, if none of the matchers match the provided string. + * + * @param matchers the matchers which should be used to match the provided string + * @param s the string to match against + * @return {@code true}, if none of the matchers match the provided string + */ + public static boolean isNoneMatch(List matchers, @Nullable CharSequence s) { + return !isAnyMatch(matchers, s); + } + + /** + * Returns the first {@link WildcardMatcher} {@linkplain WildcardMatcher#matches(CharSequence) matching} the provided string. + * + * @param matchers the matchers which should be used to match the provided string + * @param s the string to match against + * @return the first matching {@link WildcardMatcher}, or {@code null} if none match. + */ + @Nullable + public static WildcardMatcher anyMatch(List matchers, @Nullable CharSequence s) { + if (s == null || matchers.isEmpty()) { + return null; + } + return anyMatch(matchers, s, null); + } + + /** + * Returns the first {@link WildcardMatcher} {@linkplain WildcardMatcher#matches(CharSequence) matching} the provided partitioned string. + * + * @param matchers the matchers which should be used to match the provided string + * @param firstPart The first part of the string to match against. + * @param secondPart The second part of the string to match against. + * @return the first matching {@link WildcardMatcher}, or {@code null} if none match. + * @see #matches(CharSequence, CharSequence) + */ + @Nullable + public static WildcardMatcher anyMatch(List matchers, CharSequence firstPart, + @Nullable CharSequence secondPart) { + for (int i = 0; i < matchers.size(); i++) { + if (matchers.get(i).matches(firstPart, secondPart)) { + return matchers.get(i); + } + } + return null; + } + + /* + * Based on https://stackoverflow.com/a/29809553/1125055 + * Thx to Zach Vorhies + */ + public static int indexOfIgnoreCase(final CharSequence haystack1, final CharSequence haystack2, + final String needle, final boolean ignoreCase, final int start, final int end) { + if (start < 0) { + return -1; + } + int totalHaystackLength = haystack1.length() + haystack2.length(); + if (needle.isEmpty() || totalHaystackLength == 0) { + // Fallback to legacy behavior. + return indexOf(haystack1, needle); + } + + final int haystack1Length = haystack1.length(); + final int needleLength = needle.length(); + for (int i = start; i < end; i++) { + // Early out, if possible. + if (i + needleLength > totalHaystackLength) { + return -1; + } + + // Attempt to match substring starting at position i of haystack. + int j = 0; + int ii = i; + while (ii < totalHaystackLength && j < needleLength) { + char c = + ignoreCase ? Character.toLowerCase(charAt(ii, haystack1, haystack2, haystack1Length)) + : charAt(ii, haystack1, haystack2, haystack1Length); + char c2 = ignoreCase ? Character.toLowerCase(needle.charAt(j)) : needle.charAt(j); + if (c != c2) { + break; + } + j++; + ii++; + } + // Walked all the way to the end of the needle, return the start + // position that this was found. + if (j == needleLength) { + return i; + } + } + return -1; + } + + private static int indexOf(CharSequence input, String s) { + if (input instanceof StringBuilder) { + return ((StringBuilder) input).indexOf(s); + } + return input.toString().indexOf(s); + } + + static char charAt(int i, CharSequence firstPart, CharSequence secondPart, int firstPartLength) { + return i < firstPartLength ? firstPart.charAt(i) : secondPart.charAt(i - firstPartLength); + } + + + /** + * Checks if the given string matches the wildcard pattern. + * + * @param s the String to match + * @return whether the String matches the given pattern + */ + public abstract boolean matches(CharSequence s); + + /** + * This is a different version of {@link #matches(CharSequence)} which has the same semantics as calling + * {@code matcher.matches(firstPart + secondPart);}. + *

    + * The difference is that this method does not allocate memory. + *

    + * + * @param firstPart The first part of the string to match against. + * @param secondPart The second part of the string to match against. + * @return {@code true}, + * when the wildcard pattern matches the partitioned string, + * {@code false} otherwise. + */ + public abstract boolean matches(CharSequence firstPart, @Nullable CharSequence secondPart); + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof WildcardMatcher)) { + return false; + } + return toString().equals(obj.toString()); + } + + public abstract String getMatcher(); + + /** + * This {@link WildcardMatcher} supports wildcards in the middle of the matcher by decomposing the matcher into several + * {@link SimpleWildcardMatcher}s. + */ + static class CompoundWildcardMatcher extends WildcardMatcher { + private final String wildcardString; + private final String matcher; + private final List wildcardMatchers; + + CompoundWildcardMatcher(String wildcardString, String matcher, + List wildcardMatchers) { + this.wildcardString = wildcardString; + this.matcher = matcher; + this.wildcardMatchers = wildcardMatchers; + } + + @Override + public boolean matches(CharSequence s) { + int offset = 0; + for (int i = 0; i < wildcardMatchers.size(); i++) { + final SimpleWildcardMatcher matcher = wildcardMatchers.get(i); + offset = matcher.indexOf(s, offset); + if (offset == -1) { + return false; + } + offset += matcher.matcher.length(); + } + return true; + } + + @Override + public boolean matches(CharSequence firstPart, @Nullable CharSequence secondPart) { + int offset = 0; + for (int i = 0; i < wildcardMatchers.size(); i++) { + final SimpleWildcardMatcher matcher = wildcardMatchers.get(i); + offset = matcher.indexOf(firstPart, secondPart, offset); + if (offset == -1) { + return false; + } + offset += matcher.matcher.length(); + } + return true; + } + + @Override + public String toString() { + return wildcardString; + } + + @Override + public String getMatcher() { + return matcher; + } + } + + /** + * This {@link} does not support wildcards in the middle of a matcher. + */ + static class SimpleWildcardMatcher extends WildcardMatcher { + + private final String matcher; + private final String stringRepresentation; + private final boolean wildcardAtEnd; + private final boolean wildcardAtBeginning; + private final boolean ignoreCase; + + SimpleWildcardMatcher(String matcher, boolean wildcardAtBeginning, boolean wildcardAtEnd, + boolean ignoreCase) { + this.matcher = matcher; + this.wildcardAtEnd = wildcardAtEnd; + this.wildcardAtBeginning = wildcardAtBeginning; + this.ignoreCase = ignoreCase; + this.stringRepresentation = new StringBuilder( + matcher.length() + CASE_SENSITIVE_PREFIX.length() + WILDCARD.length() + WILDCARD.length()) + .append(ignoreCase ? "" : CASE_SENSITIVE_PREFIX) + .append(wildcardAtBeginning ? WILDCARD : "") + .append(matcher) + .append(wildcardAtEnd ? WILDCARD : "") + .toString(); + } + + @Override + public String toString() { + return stringRepresentation; + } + + @Override + public boolean matches(CharSequence s) { + return indexOf(s, 0) != -1; + } + + @Override + public boolean matches(CharSequence firstPart, @Nullable CharSequence secondPart) { + return indexOf(firstPart, secondPart, 0) != -1; + } + + int indexOf(final CharSequence s, final int offset) { + return indexOf(s, "", offset); + } + + int indexOf(CharSequence firstPart, @Nullable CharSequence secondPart, int offset) { + if (secondPart == null) { + secondPart = ""; + } + int totalLength = firstPart.length() + secondPart.length(); + if (wildcardAtEnd && wildcardAtBeginning) { + return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, offset, totalLength); + } else if (wildcardAtEnd) { + return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, 0, 1); + } else if (wildcardAtBeginning) { + return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, + totalLength - matcher.length(), totalLength); + } else if (totalLength == matcher.length()) { + return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, 0, totalLength); + } else { + return -1; + } + } + + @Override + public String getMatcher() { + return matcher; + } + } +} + diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/package-info.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/package-info.java deleted file mode 100644 index 8e87ae790..000000000 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/package-info.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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. - */ -@NonnullApi -package co.elastic.apm.agent.profiler; - -import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java new file mode 100644 index 000000000..f478e3b67 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.pooling; + +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; + +public abstract class AbstractObjectPool implements ObjectPool { + + protected final Allocator allocator; + protected final Resetter resetter; + private final AtomicInteger garbageCreated; + + protected AbstractObjectPool(Allocator allocator, Resetter resetter) { + this.allocator = allocator; + this.resetter = resetter; + this.garbageCreated = new AtomicInteger(); + } + + @Override + public final T createInstance() { + T object = tryCreateInstance(); + if (object == null) { + // pool does not have available instance, falling back to creating a new one + object = allocator.createInstance(); + } + return object; + } + + @Override + public final void recycle(T obj) { + resetter.recycle(obj); + if (!returnToPool(obj)) { + // when not able to return object to pool, it means this object will be garbage-collected + garbageCreated.incrementAndGet(); + } + } + + + public final long getGarbageCreated() { + return garbageCreated.longValue(); + } + + /** + * Pushes object reference back into the available pooled instances + * + * @param obj recycled object to return to pool + * @return true if object has been returned to pool, false if pool is already full + */ + abstract protected boolean returnToPool(T obj); + + /** + * Tries to create an instance in pool + * + * @return {@code null} if pool capacity is exhausted + */ + @Nullable + abstract protected T tryCreateInstance(); +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java new file mode 100644 index 000000000..2afb8dec8 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java @@ -0,0 +1,14 @@ +package co.elastic.apm.otel.profiler.pooling; + +/** + * Defines pooled object factory + * + * @param pooled object type + */ +public interface Allocator { + + /** + * @return new instance of pooled object type + */ + T createInstance(); +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java new file mode 100644 index 000000000..0ff5eaaa3 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java @@ -0,0 +1,34 @@ +package co.elastic.apm.otel.profiler.pooling; + +import org.jctools.queues.MpmcArrayQueue; + +/** + * Object pool + * + * @param pooled object type. Does not have to implement {@link Recyclable} in order to allow for dealing with objects + * that are outside of elastic apm agent (like standard JDK or third party library classes). + */ +public interface ObjectPool { + + /** + * Tries to reuse any existing instance if pool has any, otherwise creates a new un-pooled instance + * + * @return object instance, either from pool or freshly allocated + */ + T createInstance(); + + /** + * Recycles an object + * + * @param obj object to recycle + */ + void recycle(T obj); + + void clear(); + + public static ObjectPool createRecyclable(int capacity, + Allocator allocator) { + return QueueBasedObjectPool.ofRecyclable(new MpmcArrayQueue<>(capacity), false, allocator); + } + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java new file mode 100644 index 000000000..6399fbd8f --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java @@ -0,0 +1,74 @@ +package co.elastic.apm.otel.profiler.pooling; + + +import java.util.Queue; +import javax.annotation.Nullable; + +public class QueueBasedObjectPool extends AbstractObjectPool { + + private final Queue queue; + + /** + * Creates a queue based pooled for types that implement {@link Recyclable}, use {@link #of(Queue, boolean, Allocator, Resetter)} + * for other pooled object types. + * + * @param queue the underlying queue + * @param preAllocate when set to true, queue will be be pre-allocated with object instance. + * @param allocator a factory used to create new instances of the recyclable object. This factory is used when + * there are no objects in the queue and to preallocate the queue + */ + public static QueueBasedObjectPool ofRecyclable(Queue queue, + boolean preAllocate, Allocator allocator) { + return new QueueBasedObjectPool<>(queue, preAllocate, allocator, + Resetter.ForRecyclable.get()); + } + + /** + * Creates a queue based pooled for types that do not implement {@link Recyclable}, use {@link #ofRecyclable(Queue, boolean, Allocator)} + * for types that implement {@link Recyclable}. + * + * @param queue the underlying queue + * @param preAllocate when set to true, queue will be be pre-allocated with object instances fitting queue size + * @param allocator a factory used to create new instances of the recyclable object. This factory is used when + * there are no objects in the queue and to preallocate the queue + * @param resetter a reset strategy class + */ + public static QueueBasedObjectPool of(Queue queue, boolean preAllocate, + Allocator allocator, Resetter resetter) { + return new QueueBasedObjectPool<>(queue, preAllocate, allocator, resetter); + } + + private QueueBasedObjectPool(Queue queue, boolean preAllocate, + Allocator allocator, Resetter resetter) { + super(allocator, resetter); + this.queue = queue; + if (preAllocate) { + boolean addMore; + do { + addMore = queue.offer(allocator.createInstance()); + } while (addMore); + } + } + + @Nullable + @Override + public T tryCreateInstance() { + return queue.poll(); + } + + @Override + protected boolean returnToPool(T obj) { + return queue.offer(obj); + } + + public int getObjectsInPool() { + // as the size of the ring buffer is an int, this can never overflow + return queue.size(); + } + + @Override + public void clear() { + queue.clear(); + } + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java new file mode 100644 index 000000000..02f453ff6 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java @@ -0,0 +1,10 @@ +package co.elastic.apm.otel.profiler.pooling; + +public interface Recyclable { + + /** + * resets pooled object state so it can be reused + */ + void resetState(); + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java new file mode 100644 index 000000000..d848f2153 --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java @@ -0,0 +1,36 @@ +package co.elastic.apm.otel.profiler.pooling; + +/** + * Defines reset strategy to use for a given pooled object type when they are returned to pool + * + * @param pooled object type + */ +public interface Resetter { + + /** + * Recycles a pooled object state + * + * @param object object to recycle + */ + void recycle(T object); + + /** + * Resetter for objects that implement {@link Recyclable} + * + * @param recyclable object type + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + class ForRecyclable implements Resetter { + private static final ForRecyclable INSTANCE = new ForRecyclable(); + + public static Resetter get() { + return INSTANCE; + } + + @Override + public void recycle(Recyclable object) { + object.resetState(); + } + } + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java new file mode 100644 index 000000000..3bc715b2b --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java @@ -0,0 +1,25 @@ +package co.elastic.apm.otel.profiler.util; + +public class ByteUtils { + public static void putLong(byte[] buffer, int offset, long l) { + buffer[offset++] = (byte) (l >> 56); + buffer[offset++] = (byte) (l >> 48); + buffer[offset++] = (byte) (l >> 40); + buffer[offset++] = (byte) (l >> 32); + buffer[offset++] = (byte) (l >> 24); + buffer[offset++] = (byte) (l >> 16); + buffer[offset++] = (byte) (l >> 8); + buffer[offset] = (byte) l; + } + + public static long getLong(byte[] buffer, int offset) { + return ((long) buffer[offset] << 56) + | ((long) buffer[offset + 1] & 0xff) << 48 + | ((long) buffer[offset + 2] & 0xff) << 40 + | ((long) buffer[offset + 3] & 0xff) << 32 + | ((long) buffer[offset + 4] & 0xff) << 24 + | ((long) buffer[offset + 5] & 0xff) << 16 + | ((long) buffer[offset + 6] & 0xff) << 8 + | ((long) buffer[offset + 7] & 0xff); + } +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java new file mode 100644 index 000000000..98162d43e --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java @@ -0,0 +1,70 @@ +package co.elastic.apm.otel.profiler.util; + +public class HexUtils { + + public static final char[] HEX_CHARS = "0123456789abcdef".toCharArray(); + + private HexUtils() { + // only static utility methods, don't instantiate + } + + public static void appendLongAsHex(long value, StringBuilder builder) { + appendHexChar(value >> 60, builder); + appendHexChar(value >> 56, builder); + appendHexChar(value >> 52, builder); + appendHexChar(value >> 48, builder); + appendHexChar(value >> 44, builder); + appendHexChar(value >> 40, builder); + appendHexChar(value >> 36, builder); + appendHexChar(value >> 32, builder); + appendHexChar(value >> 28, builder); + appendHexChar(value >> 24, builder); + appendHexChar(value >> 20, builder); + appendHexChar(value >> 16, builder); + appendHexChar(value >> 12, builder); + appendHexChar(value >> 8, builder); + appendHexChar(value >> 4, builder); + appendHexChar(value, builder); + } + + private static void appendHexChar(long value, StringBuilder sb) { + sb.append(HEX_CHARS[(int) (value & 0x0F)]); + } + + public static long hexToLong(CharSequence hex, int offset) { + if (hex.length() - offset < 16) { + throw new IllegalStateException("Provided hex string '" + hex + "' is too short"); + } + return hexCharToBinary(hex.charAt(offset)) << 60 + | hexCharToBinary(hex.charAt(offset + 1)) << 56 + | hexCharToBinary(hex.charAt(offset + 2)) << 52 + | hexCharToBinary(hex.charAt(offset + 3)) << 48 + | hexCharToBinary(hex.charAt(offset + 4)) << 44 + | hexCharToBinary(hex.charAt(offset + 5)) << 40 + | hexCharToBinary(hex.charAt(offset + 6)) << 36 + | hexCharToBinary(hex.charAt(offset + 7)) << 32 + | hexCharToBinary(hex.charAt(offset + 8)) << 28 + | hexCharToBinary(hex.charAt(offset + 9)) << 24 + | hexCharToBinary(hex.charAt(offset + 10)) << 20 + | hexCharToBinary(hex.charAt(offset + 11)) << 16 + | hexCharToBinary(hex.charAt(offset + 12)) << 12 + | hexCharToBinary(hex.charAt(offset + 13)) << 8 + | hexCharToBinary(hex.charAt(offset + 14)) << 4 + | hexCharToBinary(hex.charAt(offset + 15)); + } + + + private static long hexCharToBinary(char ch) { + if ('0' <= ch && ch <= '9') { + return ch - '0'; + } + if ('A' <= ch && ch <= 'F') { + return ch - 'A' + 10; + } + if ('a' <= ch && ch <= 'f') { + return ch - 'a' + 10; + } + throw new IllegalArgumentException("Not a hex char: " + ch); + } + +} diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java new file mode 100644 index 000000000..b4a72c4ca --- /dev/null +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java @@ -0,0 +1,36 @@ +package co.elastic.apm.otel.profiler.util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ThreadUtils { + + private static final MethodHandle VIRTUAL_CHECKER = generateVirtualChecker(); + + + public static boolean isVirtual(Thread thread) { + try { + return (boolean) VIRTUAL_CHECKER.invokeExact(thread); + } catch (Throwable e) { + throw new IllegalStateException("isVirtual is not expected to throw exceptions", e); + } + } + + private static MethodHandle generateVirtualChecker() { + Method isVirtual = null; + try { + isVirtual = Thread.class.getMethod("isVirtual"); + isVirtual.invoke( + Thread.currentThread()); //invoke to ensure it does not throw exceptions for preview versions + return MethodHandles.lookup().unreflect(isVirtual); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + //virtual threads are not supported, therefore no thread is virtual + return MethodHandles.dropArguments( + MethodHandles.constant(boolean.class, false), + 0, + Thread.class); + } + } +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java index 52ce04308..4ac2d898a 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java @@ -16,125 +16,124 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doReturn; - -import co.elastic.apm.agent.MockReporter; -import co.elastic.apm.agent.MockTracer; -import co.elastic.apm.agent.configuration.SpyConfiguration; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.impl.transaction.Span; -import co.elastic.apm.agent.impl.transaction.StackFrame; -import co.elastic.apm.agent.impl.transaction.TraceContext; -import co.elastic.apm.agent.objectpool.NoopObjectPool; -import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; -import java.io.IOException; +package co.elastic.apm.otel.profiler; + + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import co.elastic.apm.otel.profiler.pooling.ObjectPool; +import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.OpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; -import org.stagemonitor.configuration.ConfigurationRegistry; class CallTreeSpanifyTest { - private MockReporter reporter; - private ElasticApmTracer tracer; - - @BeforeEach - void setUp() { - reporter = new MockReporter(); - ConfigurationRegistry config = SpyConfiguration.createSpyConfig(); - // disable scheduled profiling to not interfere with this test - doReturn(false).when(config.getConfig(ProfilingConfiguration.class)).isProfilingEnabled(); - tracer = MockTracer.createRealTracer(reporter, config, false); - } - - @AfterEach - void tearDown() throws IOException { - Objects.requireNonNull(tracer.getLifecycleListener(ProfilingFactory.class)) - .getProfiler() - .clear(); - reporter.assertRecycledAfterDecrementingReferences(); - tracer.stop(); + static { + //we can't reset context storage wrappers between tests, so we msut ensure that it is registered before we create ANY Otel instance + ProfilingActivationListener.ensureInitialized(); } @Test @DisabledOnOs(OS.WINDOWS) @DisabledOnAppleSilicon void testSpanification() throws Exception { - CallTree.Root callTree = - CallTreeTest.getCallTree(tracer, new String[] {" dd ", " cc ", " bbb ", "aaaaee"}); - assertThat(callTree.spanify()).isEqualTo(4); - assertThat(reporter.getSpans()).hasSize(4); - assertThat(reporter.getSpans().stream().map(Span::getNameAsString)) - .containsExactly("CallTreeTest#d", "CallTreeTest#b", "CallTreeTest#a", "CallTreeTest#e"); - - Span d = reporter.getSpans().get(0); - assertThat(d.getNameAsString()).isEqualTo("CallTreeTest#d"); - assertThat(d.getDuration()).isEqualTo(TimeUnit.MILLISECONDS.toMicros(10)); - assertThat(d.getStackFrames().stream().map(StackFrame::getMethodName)).containsExactly("c"); - - Span b = reporter.getSpans().get(1); - assertThat(b.getNameAsString()).isEqualTo("CallTreeTest#b"); - assertThat(b.getDuration()).isEqualTo(TimeUnit.MILLISECONDS.toMicros(20)); - assertThat(b.getStackFrames()).isEmpty(); - - Span a = reporter.getSpans().get(2); - assertThat(a.getNameAsString()).isEqualTo("CallTreeTest#a"); - assertThat(a.getDuration()).isEqualTo(TimeUnit.MILLISECONDS.toMicros(30)); - assertThat(a.getStackFrames()).isEmpty(); - - Span e = reporter.getSpans().get(3); - assertThat(e.getNameAsString()).isEqualTo("CallTreeTest#e"); - assertThat(e.getDuration()).isEqualTo(TimeUnit.MILLISECONDS.toMicros(10)); - assertThat(e.getStackFrames()).isEmpty(); + FixedNanoClock nanoClock = new FixedNanoClock(); + try (ProfilerTestSetup setup = ProfilerTestSetup.create(config -> config + .clock(nanoClock) + .startScheduledProfiling(false) + )) { + setup.profiler.setProfilingSessionOngoing(true); + CallTree.Root callTree = CallTreeTest.getCallTree(setup, new String[] { + " dd ", + " cc ", + " bbb ", + "aaaaee" + }); + assertThat(callTree.spanify(nanoClock, setup.sdk.getTracer("dummy-tracer"))).isEqualTo(4); + assertThat(setup.getSpans()).hasSize(5); + assertThat(setup.getSpans().stream() + .map(SpanData::getName) + ).containsExactly("Call Tree Root", "CallTreeTest#a", "CallTreeTest#b", + "CallTreeTest#d", "CallTreeTest#e"); + + SpanData a = setup.getSpans().get(1); + assertThat(a).hasName("CallTreeTest#a"); + assertThat(a.getEndEpochNanos() - a.getStartEpochNanos()).isEqualTo(30_000_000); + assertThat(a.getAttributes().get(CallTree.STACKTRACE_ATTRIBUTE_KEY)).isBlank(); + + SpanData b = setup.getSpans().get(2); + assertThat(b).hasName("CallTreeTest#b"); + assertThat(b.getEndEpochNanos() - b.getStartEpochNanos()).isEqualTo(20_000_000); + assertThat(b.getAttributes().get(CallTree.STACKTRACE_ATTRIBUTE_KEY)).isBlank(); + + SpanData d = setup.getSpans().get(3); + assertThat(d).hasName("CallTreeTest#d"); + assertThat(d.getEndEpochNanos() - d.getStartEpochNanos()).isEqualTo(10_000_000); + assertThat(d.getAttributes().get(CallTree.STACKTRACE_ATTRIBUTE_KEY)) + .isEqualTo("at " + CallTreeTest.class.getName() + ".c(CallTreeTest.java)"); + + SpanData e = setup.getSpans().get(4); + assertThat(e).hasName("CallTreeTest#e"); + assertThat(e.getEndEpochNanos() - e.getStartEpochNanos()).isEqualTo(10_000_000); + assertThat(e.getAttributes().get(CallTree.STACKTRACE_ATTRIBUTE_KEY)).isBlank(); + } + } @Test void testCallTreeWithActiveSpan() { - TraceContext rootContext = CallTreeTest.rootTraceContext(tracer); - CallTree.Root root = - CallTree.createRoot( - NoopObjectPool.ofRecyclable(() -> new CallTree.Root(tracer)), - rootContext.serialize(), - rootContext.getServiceName(), - rootContext.getServiceVersion(), - 0); - NoopObjectPool callTreePool = NoopObjectPool.ofRecyclable(CallTree::new); - root.addStackTrace(tracer, List.of(StackFrame.of("A", "a")), 0, callTreePool, 0); - - TraceContext spanContext = TraceContext.with64BitId(tracer); - TraceContext.fromParentContext().asChildOf(spanContext, rootContext); + FixedNanoClock nanoClock = new FixedNanoClock(); + + String traceId = "0af7651916cd43dd8448eb211c80319c"; + String rootSpanId = "b7ad6b7169203331"; + TraceContext rootContext = TraceContext.fromSpanContextWithZeroClockAnchor(SpanContext.create( + traceId, + rootSpanId, + TraceFlags.getSampled(), + TraceState.getDefault() + )); + + ObjectPool rootPool = ObjectPool.createRecyclable(2, CallTree.Root::new); + ObjectPool childPool = ObjectPool.createRecyclable(2, CallTree::new); + + CallTree.Root root = CallTree.createRoot(rootPool, + rootContext.serialize(), 0); + root.addStackTrace(Collections.singletonList(StackFrame.of("A", "a")), 0, childPool, 0); + + String childSpanId = "a1b2c3d4e5f64242"; + TraceContext spanContext = TraceContext.fromSpanContextWithZeroClockAnchor(SpanContext.create( + traceId, + childSpanId, + TraceFlags.getSampled(), + TraceState.getDefault() + )); root.onActivation(spanContext.serialize(), TimeUnit.MILLISECONDS.toNanos(5)); - root.addStackTrace( - tracer, - List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(10), - callTreePool, - 0); - root.addStackTrace( - tracer, - List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(20), - callTreePool, - 0); - root.onDeactivation( - spanContext.serialize(), rootContext.serialize(), TimeUnit.MILLISECONDS.toNanos(25)); - - root.addStackTrace( - tracer, - List.of(StackFrame.of("A", "a")), + root.addStackTrace(Arrays.asList(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(10), childPool, 0); + root.addStackTrace(Arrays.asList(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(20), childPool, 0); + root.onDeactivation(spanContext.serialize(), rootContext.serialize(), + TimeUnit.MILLISECONDS.toNanos(25)); + + root.addStackTrace(Collections.singletonList(StackFrame.of("A", "a")), TimeUnit.MILLISECONDS.toNanos(30), - callTreePool, - 0); - root.end(callTreePool, 0); + childPool, 0); + root.end(childPool, 0); System.out.println(root); @@ -156,10 +155,25 @@ void testCallTreeWithActiveSpan() { assertThat(b.getDurationUs()).isEqualTo(10_000); assertThat(b.getChildren()).isEmpty(); - root.spanify(); + InMemorySpanExporter exporter = InMemorySpanExporter.create(); + OpenTelemetrySdkBuilder sdkBuilder = OpenTelemetrySdk.builder() + .setTracerProvider(SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build()); + + try (OpenTelemetrySdk outputSdk = sdkBuilder.build()) { + root.spanify(nanoClock, outputSdk.getTracer("dummy-tracer")); + + List spans = exporter.getFinishedSpanItems(); + assertThat(spans).hasSize(2); + assertThat(spans.get(0)) + .hasTraceId(traceId) + .hasParentSpanId(rootSpanId); + assertThat(spans.get(1)) + .hasTraceId(traceId) + .hasParentSpanId(childSpanId); + } - assertThat(reporter.getSpans()).hasSize(2); - assertThat(reporter.getSpans().get(1).getTraceContext().isChildOf(spanContext)); - assertThat(reporter.getSpans().get(0).getTraceContext().isChildOf(rootContext)); } + } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java index d3a11e994..2294b6278 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java @@ -16,34 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static java.util.stream.Collectors.toMap; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doReturn; - -import co.elastic.apm.agent.MockReporter; -import co.elastic.apm.agent.MockTracer; -import co.elastic.apm.agent.configuration.SpyConfiguration; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.impl.sampling.ConstantSampler; -import co.elastic.apm.agent.impl.transaction.AbstractSpan; -import co.elastic.apm.agent.impl.transaction.Span; -import co.elastic.apm.agent.impl.transaction.StackFrame; -import co.elastic.apm.agent.impl.transaction.TraceContext; -import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.objectpool.NoopObjectPool; -import co.elastic.apm.agent.objectpool.ObjectPool; -import co.elastic.apm.agent.objectpool.impl.ListBasedObjectPool; -import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; -import co.elastic.apm.agent.tracer.configuration.TimeDuration; + +import co.elastic.apm.otel.profiler.pooling.ObjectPool; +import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.data.SpanData; import java.io.IOException; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -55,65 +48,42 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; -import org.stagemonitor.configuration.ConfigurationRegistry; @DisabledOnOs(OS.WINDOWS) @DisabledOnAppleSilicon class CallTreeTest { - private MockReporter reporter; - private ElasticApmTracer tracer; - private ProfilingConfiguration profilerConfig; + private ProfilerTestSetup profilerSetup; + + private FixedNanoClock nanoClock; @BeforeEach void setUp() { - reporter = new MockReporter(); - ConfigurationRegistry config = SpyConfiguration.createSpyConfig(); + nanoClock = new FixedNanoClock(); // disable scheduled profiling to not interfere with this test - profilerConfig = config.getConfig(ProfilingConfiguration.class); - doReturn(true).when(profilerConfig).isProfilingEnabled(); - tracer = MockTracer.createRealTracer(reporter, config, false); + profilerSetup = ProfilerTestSetup.create( + config -> config.clock(nanoClock).startScheduledProfiling(false)); + profilerSetup.profiler.setProfilingSessionOngoing(true); } @AfterEach void tearDown() throws IOException { - Objects.requireNonNull(tracer.getLifecycleListener(ProfilingFactory.class)) - .getProfiler() - .clear(); - tracer.stop(); + profilerSetup.close(); } @Test void testCallTree() { - TraceContext traceContext = TraceContext.with64BitId(MockTracer.create()); - CallTree.Root root = - CallTree.createRoot( - NoopObjectPool.ofRecyclable(() -> new CallTree.Root(tracer)), - traceContext.serialize(), - traceContext.getServiceName(), - traceContext.getServiceVersion(), - 0); - ObjectPool callTreePool = - ListBasedObjectPool.ofRecyclable(new ArrayList<>(), Integer.MAX_VALUE, CallTree::new); - root.addStackTrace(tracer, List.of(StackFrame.of("A", "a")), 0, callTreePool, 0); - root.addStackTrace( - tracer, - List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(10), - callTreePool, - 0); - root.addStackTrace( - tracer, - List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(20), - callTreePool, - 0); - root.addStackTrace( - tracer, - List.of(StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(30), - callTreePool, - 0); + TraceContext traceContext = new TraceContext(); + CallTree.Root root = CallTree.createRoot( + ObjectPool.createRecyclable(100, CallTree.Root::new), traceContext.serialize(), 0); + ObjectPool callTreePool = ObjectPool.createRecyclable(100, CallTree::new); + root.addStackTrace(List.of(StackFrame.of("A", "a")), 0, callTreePool, 0); + root.addStackTrace(List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(10), callTreePool, 0); + root.addStackTrace(List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(20), callTreePool, 0); + root.addStackTrace(List.of(StackFrame.of("A", "a")), TimeUnit.MILLISECONDS.toNanos(30), + callTreePool, 0); root.end(callTreePool, 0); System.out.println(root); @@ -157,113 +127,130 @@ void testGiveEmptyChildIdsTo() { assertThat(poor.hasChildIds()).isTrue(); } + @Test void testTwoDistinctInvocationsOfMethodBShouldNotBeFoldedIntoOne() throws Exception { - assertCallTree( - new String[] {" bb bb", "aaaaaa"}, - new Object[][] { - {"a", 6}, - {" b", 2}, - {" b", 2} - }); + assertCallTree(new String[] { + " bb bb", + "aaaaaa" + }, new Object[][] { + {"a", 6}, + {" b", 2}, + {" b", 2} + }); } + @Test void testBasicCallTree() throws Exception { - assertCallTree( - new String[] {" cc ", " bbb", "aaaa"}, - new Object[][] { - {"a", 4}, - {" b", 3}, - {" c", 2} - }, - new Object[][] { - {"a", 3}, - {" b", 2}, - {" c", 1} - }); + assertCallTree(new String[] { + " cc ", + " bbb", + "aaaa" + }, new Object[][] { + {"a", 4}, + {" b", 3}, + {" c", 2} + }, new Object[][] { + {"a", 3}, + {" b", 2}, + {" c", 1} + }); } @Test void testShouldNotCreateInferredSpansForPillarsAndLeafShouldHaveStacktrace() throws Exception { - assertCallTree( - new String[] {" dd ", " cc ", " bb ", "aaaa"}, - new Object[][] { - {"a", 4}, - {" b", 2}, - {" c", 2}, - {" d", 2} - }, - new Object[][] { - {"a", 3}, - {" d", 1, List.of("c", "b")} - }); + assertCallTree(new String[] { + " dd ", + " cc ", + " bb ", + "aaaa" + }, new Object[][] { + {"a", 4}, + {" b", 2}, + {" c", 2}, + {" d", 2} + }, new Object[][] { + {"a", 3}, + {" d", 1, List.of("c", "b")} + }); } @Test void testRemoveNodesWithCountOne() throws Exception { - assertCallTree( - new String[] {" b ", "aaa"}, new Object[][] {{"a", 3}}, new Object[][] {{"a", 2}}); + assertCallTree(new String[] { + " b ", + "aaa" + }, new Object[][] { + {"a", 3} + }, new Object[][] { + {"a", 2} + }); } @Test void testSameTopOfStackDifferentBottom() throws Exception { - assertCallTree( - new String[] {"cccc", "aabb"}, - new Object[][] { - {"a", 2}, - {" c", 2}, - {"b", 2}, - {" c", 2}, - }); + assertCallTree(new String[] { + "cccc", + "aabb" + }, new Object[][] { + {"a", 2}, + {" c", 2}, + {"b", 2}, + {" c", 2}, + }); } @Test void testStackTraceWithRecursion() throws Exception { - assertCallTree( - new String[] {"bbccbbcc", "bbbbbbbb", "aaaaaaaa"}, - new Object[][] { - {"a", 8}, - {" b", 8}, - {" b", 2}, - {" c", 2}, - {" b", 2}, - {" c", 2}, - }); + assertCallTree(new String[] { + "bbccbbcc", + "bbbbbbbb", + "aaaaaaaa" + }, new Object[][] { + {"a", 8}, + {" b", 8}, + {" b", 2}, + {" c", 2}, + {" b", 2}, + {" c", 2}, + }); } @Test void testFirstInferredSpanShouldHaveNoStackTrace() throws Exception { - assertCallTree( - new String[] {"bb", "aa"}, - new Object[][] { - {"a", 2}, - {" b", 2}, - }, - new Object[][] { - {"b", 1}, - }); + assertCallTree(new String[] { + "bb", + "aa" + }, new Object[][] { + {"a", 2}, + {" b", 2}, + }, new Object[][] { + {"b", 1}, + }); } @Test void testCallTreeWithSpanActivations() throws Exception { - assertCallTree( - new String[] {" cc ee ", " bbb dd ", " a aaaaaa a ", "1 2 2 1"}, - new Object[][] { - {"a", 8}, - {" b", 3}, - {" c", 2}, - {" d", 2}, - {" e", 2}, - }, - new Object[][] { - {"1", 11}, - {" a", 9}, - {" 2", 7}, - {" b", 2}, - {" c", 1}, - {" e", 1, List.of("d")}, - }); + assertCallTree(new String[] { + " cc ee ", + " bbb dd ", + " a aaaaaa a ", + "1 2 2 1" + }, new Object[][] { + {"a", 8}, + {" b", 3}, + {" c", 2}, + {" d", 2}, + {" e", 2}, + }, new Object[][] { + {"1", 11}, + {" a", 9}, + {" 2", 7}, + {" b", 2}, + {" c", 1}, + {" e", 1, List.of("d")}, + }); } /* @@ -276,28 +263,25 @@ void testCallTreeWithSpanActivations() throws Exception { */ @Test void testDeactivationBeforeEnd() throws Exception { - assertCallTree( - new String[] { - " dd ", - " cccc c ", - " bbbb bb ", // <- deactivation for span 2 happens before b and c ends - " a aaaa aa ", // that means b and c must have started before 2 has been activated - "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 - }, - new Object[][] { - {"a", 7}, - {" b", 6}, - {" c", 5}, - {" d", 2}, - }, - new Object[][] { - {"1", 10}, - {" a", 8}, - {" b", 7}, - {" c", 6}, - {" 2", 5}, - {" d", 1}, - }); + assertCallTree(new String[] { + " dd ", + " cccc c ", + " bbbb bb ", // <- deactivation for span 2 happens before b and c ends + " a aaaa aa ", // that means b and c must have started before 2 has been activated + "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 + }, new Object[][] { + {"a", 7}, + {" b", 6}, + {" c", 5}, + {" d", 2}, + }, new Object[][] { + {"1", 10}, + {" a", 8}, + {" b", 7}, + {" c", 6}, + {" 2", 5}, + {" d", 1}, + }); } /* @@ -308,19 +292,20 @@ void testDeactivationBeforeEnd() throws Exception { */ @Test void testDectivationBeforeEnd2() throws Exception { - assertCallTree( - new String[] {" bbbb b ", " a aaaa a a a ", "1 2 2 3 3 1"}, - new Object[][] { - {"a", 8}, - {" b", 5}, - }, - new Object[][] { - {"1", 13}, - {" a", 11}, - {" b", 6}, - {" 2", 5}, - {" 3", 2}, - }); + assertCallTree(new String[] { + " bbbb b ", + " a aaaa a a a ", + "1 2 2 3 3 1" + }, new Object[][] { + {"a", 8}, + {" b", 5}, + }, new Object[][] { + {"1", 13}, + {" a", 11}, + {" b", 6}, + {" 2", 5}, + {" 3", 2}, + }); } /* @@ -332,22 +317,29 @@ void testDectivationBeforeEnd2() throws Exception { */ @Test void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations() throws Exception { - Map> spans = - assertCallTree( - new String[] {" c c ", " b b ", "a a a aa", " 1 1 2 2 "}, - new Object[][] { - {"a", 5}, - {" b", 2}, - {" c", 2}, - }, - new Object[][] { - {"a", 9}, - {" 1", 2}, - {" c", 3, List.of("b")}, - {" 2", 2}, - }); - assertThat(spans.get("a").getChildIds().getSize()).isEqualTo(1); - assertThat(spans.get("c").getChildIds().getSize()).isEqualTo(1); + Map spans = assertCallTree(new String[] { + " c c ", + " b b ", + "a a a aa", + " 1 1 2 2 " + }, new Object[][] { + {"a", 5}, + {" b", 2}, + {" c", 2}, + }, new Object[][] { + {"a", 9}, + {" 1", 2}, + {" c", 3, List.of("b")}, + {" 2", 2}, + }); + assertThat(spans.get("a").getLinks()) + .hasSize(1) + .anySatisfy( + link -> assertThat(link.getAttributes()).containsEntry("elastic.is_child", true)); + assertThat(spans.get("c").getLinks()) + .hasSize(1) + .anySatisfy( + link -> assertThat(link.getAttributes()).containsEntry("elastic.is_child", true)); } /* @@ -359,23 +351,30 @@ void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations() throws E */ @Test void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations_Nested() throws Exception { - Map> spans = - assertCallTree( - new String[] {" c c ", " b b ", "a a a aa", " 1 1 23 32 "}, - new Object[][] { - {"a", 5}, - {" b", 2}, - {" c", 2}, - }, - new Object[][] { - {"a", 11}, - {" 1", 2}, - {" c", 4, List.of("b")}, - {" 2", 4}, - {" 3", 2}, - }); - assertThat(spans.get("a").getChildIds().getSize()).isEqualTo(1); - assertThat(spans.get("c").getChildIds().getSize()).isEqualTo(1); + Map spans = assertCallTree(new String[] { + " c c ", + " b b ", + "a a a aa", + " 1 1 23 32 " + }, new Object[][] { + {"a", 5}, + {" b", 2}, + {" c", 2}, + }, new Object[][] { + {"a", 11}, + {" 1", 2}, + {" c", 4, List.of("b")}, + {" 2", 4}, + {" 3", 2}, + }); + assertThat(spans.get("a").getLinks()) + .hasSize(1) + .anySatisfy( + link -> assertThat(link.getAttributes()).containsEntry("elastic.is_child", true)); + assertThat(spans.get("c").getLinks()) + .hasSize(1) + .anySatisfy( + link -> assertThat(link.getAttributes()).containsEntry("elastic.is_child", true)); } /* @@ -384,17 +383,18 @@ void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations_Nested() t */ @Test void testActivationAfterMethodEnds() throws Exception { - assertCallTree( - new String[] {"bb ", "aa a ", " 1 1"}, - new Object[][] { - {"a", 3}, - {" b", 2}, - }, - new Object[][] { - {"a", 3}, - {" b", 1}, - {" 1", 2} - }); + assertCallTree(new String[] { + "bb ", + "aa a ", + " 1 1" + }, new Object[][] { + {"a", 3}, + {" b", 2}, + }, new Object[][] { + {"a", 3}, + {" b", 1}, + {" 1", 2} + }); } /* @@ -403,17 +403,18 @@ void testActivationAfterMethodEnds() throws Exception { */ @Test void testActivationBetweenMethods() throws Exception { - assertCallTree( - new String[] {"bb ", "aa a", " 11 "}, - new Object[][] { - {"a", 3}, - {" b", 2}, - }, - new Object[][] { - {"a", 4}, - {" b", 1}, - {" 1", 1}, - }); + assertCallTree(new String[] { + "bb ", + "aa a", + " 11 " + }, new Object[][] { + {"a", 3}, + {" b", 2}, + }, new Object[][] { + {"a", 4}, + {" b", 1}, + {" 1", 1}, + }); } /* @@ -423,17 +424,19 @@ void testActivationBetweenMethods() throws Exception { */ @Test void testActivationBetweenMethods_AfterFastMethod() throws Exception { - assertCallTree( - new String[] {" c ", "bb ", "aa a", " 11 "}, - new Object[][] { - {"a", 3}, - {" b", 2}, - }, - new Object[][] { - {"a", 4}, - {" b", 1}, - {" 1", 1}, - }); + assertCallTree(new String[] { + " c ", + "bb ", + "aa a", + " 11 " + }, new Object[][] { + {"a", 3}, + {" b", 2}, + }, new Object[][] { + {"a", 4}, + {" b", 1}, + {" 1", 1}, + }); } /* @@ -443,46 +446,46 @@ void testActivationBetweenMethods_AfterFastMethod() throws Exception { */ @Test void testActivationBetweenFastMethods() throws Exception { - assertCallTree( - new String[] {"c d ", "b b ", "a a a", " 11 22 "}, - new Object[][] { - {"a", 3}, - {" b", 2}, - }, - new Object[][] { - {"a", 6}, - {" b", 3}, - {" 1", 1}, - {" 2", 1}, - }); + assertCallTree(new String[] { + "c d ", + "b b ", + "a a a", + " 11 22 " + }, new Object[][] { + {"a", 3}, + {" b", 2}, + }, new Object[][] { + {"a", 6}, + {" b", 3}, + {" 1", 1}, + {" 2", 1}, + }); } - /* */ - /* + /* *//* * [a ] * [b] [1 [c] - */ - /* - @Test - void testActivationBetweenMethods_WithCommonAncestor() throws Exception { - assertCallTree(new String[]{ - " c f g ", - "bbb e d dd", - "aaa a a aa", - " 11 22 33 " - }, new Object[][] { - {"a", 7}, - {" b", 3}, - {" d", 3}, - }, new Object[][] { - {"a", 12}, - {" b", 2}, - {" 1", 1}, - {" 2", 1}, - {" d", 4}, - {" 3", 1}, - }); - }*/ + *//* + @Test + void testActivationBetweenMethods_WithCommonAncestor() throws Exception { + assertCallTree(new String[]{ + " c f g ", + "bbb e d dd", + "aaa a a aa", + " 11 22 33 " + }, new Object[][] { + {"a", 7}, + {" b", 3}, + {" d", 3}, + }, new Object[][] { + {"a", 12}, + {" b", 2}, + {" 1", 1}, + {" 2", 1}, + {" d", 4}, + {" 3", 1}, + }); + }*/ /* * [a ] @@ -491,17 +494,16 @@ void testActivationBetweenMethods_WithCommonAncestor() throws Exception { */ @Test void testNestedActivation() throws Exception { - Map> spans = - assertCallTree( - new String[] {"a a a", " 12 21 "}, - new Object[][] { - {"a", 3}, - }, - new Object[][] { - {"a", 6}, - {" 1", 4}, - {" 2", 2}, - }); + assertCallTree(new String[] { + "a a a", + " 12 21 " + }, new Object[][] { + {"a", 3}, + }, new Object[][] { + {"a", 6}, + {" 1", 4}, + {" 2", 2}, + }); } /* @@ -512,25 +514,23 @@ void testNestedActivation() throws Exception { */ @Test void testNestedActivationAfterMethodEnds_RootChangesToC() throws Exception { - Map> spans = - assertCallTree( - new String[] {" bbb ", " aaa ccc ", "1 23 321"}, - new Object[][] { - {"a", 3}, - {" b", 3}, - {"c", 3}, - }, - new Object[][] { - {"1", 11}, - {" b", 2, List.of("a")}, - {" 2", 6}, - {" 3", 4}, - {" c", 2} - }); - - if (spans.get("b").getChildIds() != null) { - assertThat(spans.get("b").getChildIds().isEmpty()).isTrue(); - } + Map spans = assertCallTree(new String[] { + " bbb ", + " aaa ccc ", + "1 23 321" + }, new Object[][] { + {"a", 3}, + {" b", 3}, + {"c", 3}, + }, new Object[][] { + {"1", 11}, + {" b", 2, List.of("a")}, + {" 2", 6}, + {" 3", 4}, + {" c", 2} + }); + + assertThat(spans.get("b").getLinks()).isEmpty(); } /* @@ -541,21 +541,23 @@ void testNestedActivationAfterMethodEnds_RootChangesToC() throws Exception { */ @Test void testRegularActivationFollowedByNestedActivationAfterMethodEnds() throws Exception { - assertCallTree( - new String[] {" d ", " b b b ", " a a a ccc ", "1 2 2 34 431"}, - new Object[][] { - {"a", 3}, - {" b", 3}, - {"c", 3}, - }, - new Object[][] { - {"1", 13}, - {" b", 4, List.of("a")}, - {" 2", 2}, - {" 3", 6}, - {" 4", 4}, - {" c", 2} - }); + assertCallTree(new String[] { + " d ", + " b b b ", + " a a a ccc ", + "1 2 2 34 431" + }, new Object[][] { + {"a", 3}, + {" b", 3}, + {"c", 3}, + }, new Object[][] { + {"1", 13}, + {" b", 4, List.of("a")}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} + }); } /* @@ -567,30 +569,43 @@ void testRegularActivationFollowedByNestedActivationAfterMethodEnds() throws Exc */ @Test void testNestedActivationAfterMethodEnds_CommonAncestorA() throws Exception { - Map> spans = - assertCallTree( - new String[] {" b b b ccc ", " aa a a aaa a ", "1 2 2 34 43 1"}, - new Object[][] { - {"a", 8}, - {" b", 3}, - {" c", 3}, - }, - new Object[][] { - {"1", 15}, - {" a", 13}, - {" b", 4}, - {" 2", 2}, - {" 3", 6}, - {" 4", 4}, - {" c", 2} - }); - - assertThat(spans.get("b").getChildIds().toArray()) - .containsExactly(spans.get("2").getTraceContext().getId().readLong(0)); - assertThat(spans.get("c").getChildIds()).isNull(); - // only has 3 as a child as 4 is a nested activation - assertThat(spans.get("a").getChildIds().toArray()) - .containsExactly(spans.get("3").getTraceContext().getId().readLong(0)); + Map spans = assertCallTree(new String[] { + " b b b ccc ", + " aa a a aaa a ", + "1 2 2 34 43 1" + }, new Object[][] { + {"a", 8}, + {" b", 3}, + {" c", 3}, + }, new Object[][] { + {"1", 15}, + {" a", 13}, + {" b", 4}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} + }); + + assertThat(spans.get("b").getLinks()) + .hasSize(1) + .anySatisfy(link -> { + assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); + SpanData expectedSpan = spans.get("2"); + assertThat(link.getSpanContext().getTraceId()).isEqualTo(expectedSpan.getTraceId()); + assertThat(link.getSpanContext().getSpanId()).isEqualTo(expectedSpan.getSpanId()); + }); + + assertThat(spans.get("c").getLinks()).isEmpty(); + + assertThat(spans.get("a").getLinks()) + .hasSize(1) + .anySatisfy(link -> { + assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); + SpanData expectedSpan = spans.get("3"); + assertThat(link.getSpanContext().getTraceId()).isEqualTo(expectedSpan.getTraceId()); + assertThat(link.getSpanContext().getSpanId()).isEqualTo(expectedSpan.getSpanId()); + }); } /* @@ -602,19 +617,20 @@ void testNestedActivationAfterMethodEnds_CommonAncestorA() throws Exception { */ @Test void testActivationAfterMethodEnds_RootChangesToB() throws Exception { - assertCallTree( - new String[] {" ccc ", " aaa bbb ", "1 2 21"}, - new Object[][] { - {"a", 3}, - {"b", 3}, - {" c", 3}, - }, - new Object[][] { - {"1", 9}, - {" a", 2}, - {" 2", 4}, - {" c", 2, List.of("b")} - }); + assertCallTree(new String[] { + " ccc ", + " aaa bbb ", + "1 2 21" + }, new Object[][] { + {"a", 3}, + {"b", 3}, + {" c", 3}, + }, new Object[][] { + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" c", 2, List.of("b")} + }); } /* @@ -625,35 +641,37 @@ void testActivationAfterMethodEnds_RootChangesToB() throws Exception { */ @Test void testActivationAfterMethodEnds_RootChangesToB2() throws Exception { - assertCallTree( - new String[] {" aaa bbb ", "1 2 21"}, - new Object[][] { - {"a", 3}, - {"b", 3}, - }, - new Object[][] { - {"1", 9}, - {" a", 2}, - {" 2", 4}, - {" b", 2} - }); + assertCallTree(new String[] { + " aaa bbb ", + "1 2 21" + }, new Object[][] { + {"a", 3}, + {"b", 3}, + }, new Object[][] { + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" b", 2} + }); } - /* - * [1] - * [a] - @Test - void testActivationBeforeCallTree() throws Exception { - assertCallTree(new String[]{ - " aaa", - "1 1 " - }, new Object[][] { - {"a", 3}, - }, new Object[][] { - {"a", 3}, - {" 1", 2}, - }); - } */ + + /* + * [1] + * [a] + @Test + void testActivationBeforeCallTree() throws Exception { + assertCallTree(new String[]{ + " aaa", + "1 1 " + }, new Object[][] { + {"a", 3}, + }, new Object[][] { + {"a", 3}, + {" 1", 2}, + }); + } */ + /* * [1 ] @@ -664,18 +682,19 @@ void testActivationBeforeCallTree() throws Exception { */ @Test void testActivationAfterMethodEnds_SameRootDeeperStack() throws Exception { - assertCallTree( - new String[] {" ccc ", " aaa aaa ", "1 2 21"}, - new Object[][] { - {"a", 6}, - {" c", 3}, - }, - new Object[][] { - {"1", 9}, - {" a", 6}, - {" 2", 4}, - {" c", 2} - }); + assertCallTree(new String[] { + " ccc ", + " aaa aaa ", + "1 2 21" + }, new Object[][] { + {"a", 6}, + {" c", 3}, + }, new Object[][] { + {"1", 9}, + {" a", 6}, + {" 2", 4}, + {" c", 2} + }); } /* @@ -686,18 +705,19 @@ void testActivationAfterMethodEnds_SameRootDeeperStack() throws Exception { */ @Test void testActivationBeforeMethodStarts() throws Exception { - assertCallTree( - new String[] {" bbb ", " a aaa a ", "1 2 2 1"}, - new Object[][] { - {"a", 5}, - {" b", 3}, - }, - new Object[][] { - {"1", 8}, - {" a", 6}, - {" 2", 4}, - {" b", 2} - }); + assertCallTree(new String[] { + " bbb ", + " a aaa a ", + "1 2 2 1" + }, new Object[][] { + {"a", 5}, + {" b", 3}, + }, new Object[][] { + {"1", 8}, + {" a", 6}, + {" 2", 4}, + {" b", 2} + }); } /* @@ -710,110 +730,133 @@ void testActivationBeforeMethodStarts() throws Exception { */ @Test void testDectivationAfterEnd() throws Exception { - assertCallTree( - new String[] { - " dd ", - " c ccc ", - " bb bbb ", // <- deactivation for span 2 happens after b ends - " aaa aaa aa ", // that means b must have ended after 2 has been deactivated - "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 - }, - new Object[][] { - {"a", 8}, - {" b", 5}, - {" c", 4}, - {" d", 2}, - }, - new Object[][] { - {"1", 11}, - {" a", 9}, - {" b", 6}, - {" c", 5}, - {" 2", 4}, - {" d", 1}, - }); + assertCallTree(new String[] { + " dd ", + " c ccc ", + " bb bbb ", // <- deactivation for span 2 happens after b ends + " aaa aaa aa ", // that means b must have ended after 2 has been deactivated + "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 + }, new Object[][] { + {"a", 8}, + {" b", 5}, + {" c", 4}, + {" d", 2}, + }, new Object[][] { + {"1", 11}, + {" a", 9}, + {" b", 6}, + {" c", 5}, + {" 2", 4}, + {" d", 1}, + }); } @Test void testCallTreeActivationAsParentOfFastSpan() throws Exception { - assertCallTree( - new String[] {" b ", " aa a aa ", "1 2 2 1"}, - new Object[][] {{"a", 5}}, - new Object[][] { - {"1", 8}, - {" a", 6}, - {" 2", 2}, - }); + assertCallTree(new String[] { + " b ", + " aa a aa ", + "1 2 2 1" + }, new Object[][] { + {"a", 5} + }, new Object[][] { + {"1", 8}, + {" a", 6}, + {" 2", 2}, + }); } @Test void testCallTreeActivationAsChildOfFastSpan() throws Exception { - doReturn(TimeDuration.of("50ms")).when(profilerConfig).getInferredSpansMinDuration(); - assertCallTree( - new String[] {" c c ", " b b ", " aaa aaa ", "1 22 1"}, - new Object[][] {{"a", 6}}, - new Object[][] { - {"1", 9}, - {" a", 7}, - {" 2", 1}, - }); + profilerSetup.close(); + profilerSetup = ProfilerTestSetup.create( + config -> config + .inferredSpansMinDuration(Duration.ofMillis(50)).clock(nanoClock) + .startScheduledProfiling(false)); + profilerSetup.profiler.setProfilingSessionOngoing(true); + assertCallTree(new String[] { + " c c ", + " b b ", + " aaa aaa ", + "1 22 1" + }, new Object[][] { + {"a", 6} + }, new Object[][] { + {"1", 9}, + {" a", 7}, + {" 2", 1}, + }); } @Test void testCallTreeActivationAsLeaf() throws Exception { - assertCallTree( - new String[] {" aa aa ", "1 22 1"}, - new Object[][] {{"a", 4}}, - new Object[][] { - {"1", 7}, - {" a", 5}, - {" 2", 1}, - }); + assertCallTree(new String[] { + " aa aa ", + "1 22 1" + }, new Object[][] { + {"a", 4} + }, new Object[][] { + {"1", 7}, + {" a", 5}, + {" 2", 1}, + }); } + @Test void testCallTreeMultipleActivationsAsLeaf() throws Exception { - assertCallTree( - new String[] {" aa aaa aa ", "1 22 33 1"}, - new Object[][] {{"a", 7}}, - new Object[][] { - {"1", 12}, - {" a", 10}, - {" 2", 1}, - {" 3", 1}, - }); + assertCallTree(new String[] { + " aa aaa aa ", + "1 22 33 1" + }, new Object[][] { + {"a", 7} + }, new Object[][] { + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, + }); } @Test void testCallTreeMultipleActivationsAsLeafWithExcludedParent() throws Exception { - doReturn(TimeDuration.of("50ms")).when(profilerConfig).getInferredSpansMinDuration(); + profilerSetup.close(); + profilerSetup = ProfilerTestSetup.create( + config -> config.clock(nanoClock) + .startScheduledProfiling(false) + .inferredSpansMinDuration(Duration.ofMillis(50))); + profilerSetup.profiler.setProfilingSessionOngoing(true); // min duration 4 - assertCallTree( - new String[] {" b b c c ", " aa aaa aa ", "1 22 33 1"}, - new Object[][] {{"a", 7}}, - new Object[][] { - {"1", 12}, - {" a", 10}, - {" 2", 1}, - {" 3", 1}, - }); + assertCallTree(new String[] { + " b b c c ", + " aa aaa aa ", + "1 22 33 1" + }, new Object[][] { + {"a", 7} + }, new Object[][] { + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, + }); } @Test void testCallTreeMultipleActivationsWithOneChild() throws Exception { - assertCallTree( - new String[] {" bb ", " aa aaa aa aa ", "1 22 3 3 1"}, - new Object[][] { - {"a", 9}, - {" b", 2} - }, - new Object[][] { - {"1", 14}, - {" a", 12}, - {" 2", 1}, - {" 3", 3}, - {" b", 1}, - }); + assertCallTree(new String[] { + " bb ", + " aa aaa aa aa ", + "1 22 3 3 1" + }, new Object[][] { + {"a", 9}, + {" b", 2} + }, new Object[][] { + {"1", 14}, + {" a", 12}, + {" 2", 1}, + {" 3", 3}, + {" b", 1}, + }); } /* @@ -826,26 +869,25 @@ void testCallTreeMultipleActivationsWithOneChild() throws Exception { @Test @Disabled("fix me") void testNestedActivationBeforeCallTree() throws Exception { - assertCallTree( - new String[] {" aaa ", "12 2 1"}, - new Object[][] { - {"a", 3}, - }, - new Object[][] { - {"1", 5}, - {" a", 3}, // a is actually a child of the transaction - {" 2", 2}, // 2 is not within the child_ids of a - }); + assertCallTree(new String[] { + " aaa ", + "12 2 1" + }, new Object[][] { + {"a", 3}, + }, new Object[][] { + {"1", 5}, + {" a", 3}, // a is actually a child of the transaction + {" 2", 2}, // 2 is not within the child_ids of a + }); } private void assertCallTree(String[] stackTraces, Object[][] expectedTree) throws Exception { assertCallTree(stackTraces, expectedTree, null); } - private Map> assertCallTree( - String[] stackTraces, Object[][] expectedTree, @Nullable Object[][] expectedSpans) - throws Exception { - CallTree.Root root = getCallTree(tracer, stackTraces); + private Map assertCallTree(String[] stackTraces, Object[][] expectedTree, + @Nullable Object[][] expectedSpans) throws Exception { + CallTree.Root root = getCallTree(profilerSetup, stackTraces); StringBuilder expectedResult = new StringBuilder(); for (int i = 0; i < expectedTree.length; i++) { Object[] objects = expectedTree[i]; @@ -855,24 +897,25 @@ private Map> assertCallTree( } } - String actualResult = root.toString().replace(CallTreeTest.class.getName() + ".", ""); - actualResult = - Arrays.stream(actualResult.split("\n")) - // skip root node - .skip(1) - // trim first two spaces - .map(s -> s.substring(2)) - .collect(Collectors.joining("\n")); + String actualResult = root.toString() + .replace(CallTreeTest.class.getName() + ".", ""); + actualResult = Arrays.stream(actualResult.split("\n")) + // skip root node + .skip(1) + // trim first two spaces + .map(s -> s.substring(2)) + .collect(Collectors.joining("\n")); assertThat(actualResult).isEqualTo(expectedResult.toString()); if (expectedSpans != null) { - root.spanify(); - Map> spans = - reporter.getSpans().stream() - .collect(toMap(s -> s.getNameAsString().replaceAll(".*#", ""), Function.identity())); - assertThat(reporter.getSpans()).hasSize(expectedSpans.length); - spans.put(null, reporter.getTransactions().get(0)); + root.spanify(nanoClock, profilerSetup.sdk.getTracer("dummy-inferred-spans-tracer")); + Map spans = profilerSetup.getSpans() + .stream() + .collect(toMap( + s -> s.getName().replaceAll(".*#", ""), + Function.identity())); + assertThat(profilerSetup.getSpans()).hasSize(expectedSpans.length + 1); for (int i = 0; i < expectedSpans.length; i++) { Object[] expectedSpan = expectedSpans[i]; @@ -882,60 +925,77 @@ private Map> assertCallTree( expectedSpan.length == 3 ? (List) expectedSpan[2] : List.of(); int nestingLevel = getNestingLevel((String) expectedSpan[0]); String parentName = getParentName(expectedSpans, i, nestingLevel); + if (parentName == null) { + parentName = "Call Tree Root"; + } assertThat(spans).containsKey(spanName); assertThat(spans).containsKey(parentName); - AbstractSpan span = spans.get(spanName); - assertThat(span.isChildOf(spans.get(parentName))) - .withFailMessage( - "Expected %s (%s) to be a child of %s (%s) but was %s (%s)", - spanName, - span.getTraceContext().getId(), - parentName, - spans.get(parentName).getTraceContext().getId(), - reporter.getSpans().stream() - .filter( - s -> - s.getTraceContext() - .getId() - .equals(span.getTraceContext().getParentId())) - .findAny() - .map(Span::getNameAsString) + SpanData span = spans.get(spanName); + assertThat(isChild(spans.get(parentName), span)) + .withFailMessage("Expected %s (%s) to be a child of %s (%s) but was %s (%s)", + spanName, span.getSpanContext().getSpanId(), + parentName, spans.get(parentName).getSpanId(), + profilerSetup.getSpans() + .stream() + .filter(s -> s.getSpanId() + .equals(span.getParentSpanId())).findAny() + .map(SpanData::getName) .orElse(null), - span.getTraceContext().getParentId()) + span.getParentSpanId()) .isTrue(); - assertThat(spans.get(parentName).isChildOf(span)) - .withFailMessage( - "Expected %s (%s) to not be a child of %s (%s) but was %s (%s)", - parentName, - spans.get(parentName).getTraceContext().getId(), - spanName, - span.getTraceContext().getId(), - reporter.getSpans().stream() - .filter( - s -> - s.getTraceContext() - .getId() - .equals(span.getTraceContext().getParentId())) - .findAny() - .map(Span::getNameAsString) + assertThat(isChild(span, spans.get(parentName))) + .withFailMessage("Expected %s (%s) to not be a child of %s (%s) but was %s (%s)", + parentName, spans.get(parentName).getSpanId(), + spanName, span.getSpanId(), + profilerSetup.getSpans() + .stream() + .filter(s -> s.getSpanId() + .equals(span.getParentSpanId())).findAny() + .map(SpanData::getName) .orElse(null), - span.getTraceContext().getParentId()) + span.getParentSpanId()) .isFalse(); - assertThat(span.getDuration()) + assertThat(span.getEndEpochNanos() - span.getStartEpochNanos()) .describedAs("Unexpected duration for span %s", span) - .isEqualTo(durationMs * 1000); - assertThat( - Objects.requireNonNullElse(((Span) span).getStackFrames(), List.of()) - .stream() - .map(StackFrame::getMethodName) - .collect(Collectors.toList())) - .isEqualTo(stackTrace); + .isEqualTo(durationMs * 1_000_000L); + + String actualStacktrace = span.getAttributes().get(CallTree.STACKTRACE_ATTRIBUTE_KEY); + if (stackTrace == null || stackTrace.isEmpty()) { + assertThat(actualStacktrace).isBlank(); + } else { + String expected = stackTrace.stream() + .map(funcName -> "at " + CallTreeTest.class.getName() + "." + funcName + + "(CallTreeTest.java)") + .collect(Collectors.joining("\n")); + assertThat(actualStacktrace).isEqualTo(expected); + } } return spans; } return null; } + public boolean isChild(SpanData parent, SpanData expectedChild) { + if (!parent.getTraceId().equals(expectedChild.getTraceId())) { + return false; + } + if (parent.getSpanId().equals(expectedChild.getParentSpanId())) { + return true; + } + for (LinkData link : parent.getLinks()) { + Boolean isChild = link.getAttributes().get(CallTree.IS_CHILD_ATTRIBUTE_KEY); + if (isChild != null && isChild) { + SpanContext linkSpanCtx = link.getSpanContext(); + if (linkSpanCtx.getTraceId().equals(expectedChild.getTraceId()) && linkSpanCtx.getSpanId() + .equals(expectedChild.getSpanId())) { + return true; + } + } + } + + return false; + } + @Nullable private String getParentName(@Nonnull Object[][] expectedSpans, int i, int nestingLevel) { if (nestingLevel > 0) { @@ -955,59 +1015,60 @@ private int getNestingLevel(String spanName) { return ((spanName).length() - 1) / 2; } - public static CallTree.Root getCallTree(ElasticApmTracer tracer, String[] stackTraces) + public static CallTree.Root getCallTree(ProfilerTestSetup profilerSetup, String[] stackTraces) throws Exception { - ProfilingFactory profilingFactory = tracer.getLifecycleListener(ProfilingFactory.class); - assertThat(profilingFactory).isNotNull(); - - SamplingProfiler profiler = profilingFactory.getProfiler(); - FixedNanoClock nanoClock = (FixedNanoClock) profilingFactory.getNanoClock(); - nanoClock.setNanoTime(0); + SamplingProfiler profiler = profilerSetup.profiler; + FixedNanoClock nanoClock = (FixedNanoClock) profilerSetup.profiler.getClock(); + nanoClock.setNanoTime(1); profiler.setProfilingSessionOngoing(true); - Transaction transaction = - tracer - .startRootTransaction(ConstantSampler.of(true), 0, null) - .withName("Call Tree Root") - .activate(); - transaction.getTraceContext().getClock().init(0, 0); - Map> spanMap = new HashMap<>(); - List stackTraceEvents = new ArrayList<>(); - for (int i = 0; i < stackTraces[0].length(); i++) { - nanoClock.setNanoTime(i * TimeUnit.MILLISECONDS.toNanos(10)); - List trace = new ArrayList<>(); - for (String stackTrace : stackTraces) { - char c = stackTrace.charAt(i); - if (Character.isDigit(c)) { - handleSpanEvent(tracer, spanMap, Character.toString(c), nanoClock.nanoTime()); - break; - } else if (!Character.isSpaceChar(c)) { - trace.add(StackFrame.of(CallTreeTest.class.getName(), Character.toString(c))); + + CallTree.Root root = null; + ObjectPool callTreePool = ObjectPool.createRecyclable(2, CallTree::new); + Map spanMap = new HashMap<>(); + Map spanScopeMap = new HashMap<>(); + + Tracer tracer = profilerSetup.sdk.getTracer("testing-tracer"); + + Span transaction = tracer.spanBuilder("Call Tree Root") + .setStartTimestamp(1, TimeUnit.NANOSECONDS) + .startSpan(); + try (Scope scope = transaction.makeCurrent()) { + List stackTraceEvents = new ArrayList<>(); + for (int i = 0; i < stackTraces[0].length(); i++) { + nanoClock.setNanoTime(1 + i * TimeUnit.MILLISECONDS.toNanos(10)); + List trace = new ArrayList<>(); + for (String stackTrace : stackTraces) { + char c = stackTrace.charAt(i); + if (Character.isDigit(c)) { + handleSpanEvent(tracer, spanMap, spanScopeMap, Character.toString(c), + nanoClock.nanoTime()); + break; + } else if (!Character.isSpaceChar(c)) { + trace.add(StackFrame.of(CallTreeTest.class.getName(), Character.toString(c))); + } + } + if (!trace.isEmpty()) { + stackTraceEvents.add(new StackTraceEvent(trace, nanoClock.nanoTime())); } } - if (!trace.isEmpty()) { - stackTraceEvents.add(new StackTraceEvent(trace, nanoClock.nanoTime())); - } - } - profiler.consumeActivationEventsFromRingBufferAndWriteToFile(); - long eof = profiler.startProcessingActivationEventsFile(); - CallTree.Root root = null; - NoopObjectPool callTreePool = NoopObjectPool.ofRecyclable(CallTree::new); - for (StackTraceEvent stackTraceEvent : stackTraceEvents) { - profiler.processActivationEventsUpTo(stackTraceEvent.nanoTime, eof); - if (root == null) { - root = profiler.getRoot(); - assertThat(root).isNotNull(); + + profiler.consumeActivationEventsFromRingBufferAndWriteToFile(); + long eof = profiler.startProcessingActivationEventsFile(); + for (StackTraceEvent stackTraceEvent : stackTraceEvents) { + profiler.processActivationEventsUpTo(stackTraceEvent.nanoTime, eof); + if (root == null) { + root = profiler.getRoot(); + assertThat(root).isNotNull(); + } + long millis = profilerSetup.profiler.config.getInferredSpansMinDuration().toMillis(); + root.addStackTrace(stackTraceEvent.trace, stackTraceEvent.nanoTime, callTreePool, + TimeUnit.MILLISECONDS.toNanos(millis)); } - long millis = - tracer.getConfig(ProfilingConfiguration.class).getInferredSpansMinDuration().getMillis(); - root.addStackTrace( - tracer, - stackTraceEvent.trace, - stackTraceEvent.nanoTime, - callTreePool, - TimeUnit.MILLISECONDS.toNanos(millis)); + + } finally { + transaction.end(); } - transaction.deactivate().end(nanoClock.nanoTime() / 1000); + assertThat(root).isNotNull(); root.end(callTreePool, 0); return root; @@ -1025,19 +1086,20 @@ public StackTraceEvent(List trace, long nanoTime) { } } - private static void handleSpanEvent( - ElasticApmTracer tracer, Map> spanMap, String name, long nanoTime) { + private static void handleSpanEvent(Tracer tracer, Map spanMap, + Map spanScopeMap, + String name, long nanoTime) { if (!spanMap.containsKey(name)) { - Span span = tracer.getActive().createSpan(nanoTime / 1000).appendToName(name).activate(); + Span span = tracer.spanBuilder(name) + .setParent(Context.current()) + .setStartTimestamp(nanoTime, TimeUnit.NANOSECONDS) + .startSpan(); spanMap.put(name, span); + spanScopeMap.put(name, span.makeCurrent()); } else { - spanMap.get(name).deactivate().end(nanoTime / 1000); + spanScopeMap.remove(name).close(); + spanMap.get(name).end(nanoTime, TimeUnit.NANOSECONDS); } } - public static TraceContext rootTraceContext(ElasticApmTracer tracer) { - TraceContext traceContext = TraceContext.with64BitId(tracer); - traceContext.asRootSpan(ConstantSampler.of(true)); - return traceContext; - } } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/FixedNanoClock.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java similarity index 66% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/FixedNanoClock.java rename to inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java index 97fa892b8..6b1f59018 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/FixedNanoClock.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java @@ -16,12 +16,22 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; + +import co.elastic.apm.otel.profiler.NanoClock; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; public class FixedNanoClock implements NanoClock { private long nanoTime = -1L; + @Override + public void onSpanStart(ReadWriteSpan started, Context parentContext) { + + } + @Override public long nanoTime() { if (nanoTime == -1L) { @@ -30,6 +40,21 @@ public long nanoTime() { return nanoTime; } + @Override + public long getAnchor(Span parent) { + return 0; + } + + @Override + public long toEpochNanos(long anchor, long recordedNanoTime) { + return recordedNanoTime; + } + + @Override + public void periodicCleanup() { + + } + public void setNanoTime(long nanoTime) { this.nanoTime = nanoTime; } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java new file mode 100644 index 000000000..694d32c7d --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java @@ -0,0 +1,57 @@ +package co.elastic.apm.otel.profiler; + +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.util.List; +import java.util.function.Consumer; + +public class ProfilerTestSetup implements AutoCloseable { + + OpenTelemetrySdk sdk; + + SamplingProfiler profiler; + + InMemorySpanExporter spanExporter; + + + public ProfilerTestSetup(OpenTelemetrySdk sdk, InferredSpansProcessor processor, + InMemorySpanExporter spanExporter) { + this.sdk = sdk; + this.profiler = processor.profiler; + this.spanExporter = spanExporter; + } + + public List getSpans() { + return spanExporter.getFinishedSpanItems(); + } + + @Override + public void close() { + sdk.close(); + } + + public static ProfilerTestSetup create(Consumer configCustomizer) { + InferredSpansProcessorBuilder builder = InferredSpansConfiguration.builder(); + configCustomizer.accept(builder); + + InferredSpansProcessor processor = builder.build(); + + InMemorySpanExporter exporter = InMemorySpanExporter.create(); + + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(processor) + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build(); + processor.setTracerProvider(tracerProvider); + + OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .build(); + + return new ProfilerTestSetup(sdk, processor, exporter); + } + +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java index ad7b87026..dde30545d 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java @@ -16,16 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; -import co.elastic.apm.agent.MockTracer; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.impl.transaction.TraceContext; -import co.elastic.apm.agent.objectpool.ObjectPoolFactory; -import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; +import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.OS; @@ -36,30 +35,34 @@ public class SamplingProfilerQueueTest { @DisabledOnOs(OS.WINDOWS) @DisabledOnAppleSilicon void testFillQueue() throws Exception { - System.out.println(System.getProperty("os.name")); - ElasticApmTracer tracer = MockTracer.create(); - when(tracer.getObjectPoolFactory()).thenReturn(new ObjectPoolFactory()); + try (ProfilerTestSetup setup = ProfilerTestSetup.create( + config -> config.clock(new FixedNanoClock()).startScheduledProfiling(false))) { - SamplingProfiler profiler = new SamplingProfiler(tracer, new SystemNanoClock()); + setup.profiler.setProfilingSessionOngoing(true); - profiler.setProfilingSessionOngoing(true); - TraceContext traceContext = TraceContext.with64BitId(tracer); + Span traceContext = Span.wrap( + SpanContext.create( + "0af7651916cd43dd8448eb211c80319c", + "b7ad6b7169203331", + TraceFlags.getSampled(), + TraceState.getDefault() + )); - assertThat(profiler.onActivation(traceContext, null)).isTrue(); - long timeAfterFirstEvent = System.nanoTime(); - Thread.sleep(1); + assertThat(setup.profiler.onActivation(traceContext, null)).isTrue(); - for (int i = 0; i < SamplingProfiler.RING_BUFFER_SIZE - 1; i++) { - assertThat(profiler.onActivation(traceContext, null)).isTrue(); - } + for (int i = 0; i < SamplingProfiler.RING_BUFFER_SIZE - 1; i++) { + assertThat(setup.profiler.onActivation(traceContext, null)).isTrue(); + } + + // no more free slots after adding RING_BUFFER_SIZE events + assertThat(setup.profiler.onActivation(traceContext, null)).isFalse(); - // no more free slots after adding RING_BUFFER_SIZE events - assertThat(profiler.onActivation(traceContext, null)).isFalse(); + setup.profiler.consumeActivationEventsFromRingBufferAndWriteToFile(); - profiler.consumeActivationEventsFromRingBufferAndWriteToFile(); + // now there should be free slots + assertThat(setup.profiler.onActivation(traceContext, null)).isTrue(); + } - // now there should be free slots - assertThat(profiler.onActivation(traceContext, null)).isTrue(); } } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java index bc496b587..5e43a9ead 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java @@ -16,28 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; -import co.elastic.apm.agent.MockReporter; -import co.elastic.apm.agent.MockTracer; -import co.elastic.apm.agent.sdk.logging.Logger; -import co.elastic.apm.agent.sdk.logging.LoggerFactory; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; /** - * Can be used in combination with the files created by {@link - * ProfilingConfiguration#backupDiagnosticFiles} to replay the creation of profiler-inferred spans. - * This is useful, for example, to troubleshoot why {@link - * co.elastic.apm.agent.impl.transaction.Span#childIds} are set as expected. + * Can be used in combination with the files created by + * {@link ProfilingConfiguration#backupDiagnosticFiles} to replay the creation of profiler-inferred spans. + * This is useful, for example, to troubleshoot why {@link co.elastic.apm.agent.impl.transaction.Span#childIds} are set as expected. */ public class SamplingProfilerReplay { - private static final Logger logger = LoggerFactory.getLogger(SamplingProfilerReplay.class); + private static final Logger logger = Logger.getLogger(SamplingProfilerReplay.class.getName()); public static void main(String[] args) throws Exception { ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true); @@ -45,32 +42,29 @@ public static void main(String[] args) throws Exception { activationEventsFile.deleteOnExit(); File jfrFile = File.createTempFile("traces", ".jfr"); jfrFile.deleteOnExit(); - MockReporter reporter = new MockReporter(); - SamplingProfiler samplingProfiler = - new SamplingProfiler( - MockTracer.createRealTracer(reporter), - new SystemNanoClock(), - activationEventsFile, - jfrFile); - Path baseDir = Paths.get(System.getProperty("java.io.tmpdir"), "profiler"); - List activationFiles = - Files.list(baseDir) - .filter(p -> p.toString().endsWith("activations.dat")) - .sorted() - .collect(Collectors.toList()); - List traceFiles = - Files.list(baseDir) - .filter(p -> p.toString().endsWith("traces.jfr")) - .sorted() - .collect(Collectors.toList()); - if (traceFiles.size() != activationFiles.size()) { - throw new IllegalStateException(); - } - for (int i = 0; i < activationFiles.size(); i++) { - logger.info("processing {} {}", activationFiles.get(i), traceFiles.get(i)); - samplingProfiler.copyFromFiles(activationFiles.get(i), traceFiles.get(i)); - samplingProfiler.processTraces(); + + try (ProfilerTestSetup setup = ProfilerTestSetup.create( + config -> config + .startScheduledProfiling(false) + .activationEventsFile(activationEventsFile) + .jfrFile(jfrFile) + )) { + Path baseDir = Paths.get(System.getProperty("java.io.tmpdir"), "profiler"); + List activationFiles = Files.list(baseDir) + .filter(p -> p.toString().endsWith("activations.dat")).sorted() + .collect(Collectors.toList()); + List traceFiles = Files.list(baseDir).filter(p -> p.toString().endsWith("traces.jfr")) + .sorted().collect(Collectors.toList()); + if (traceFiles.size() != activationFiles.size()) { + throw new IllegalStateException(); + } + for (int i = 0; i < activationFiles.size(); i++) { + logger.log(Level.INFO, "processing {0} {1}", + new Object[] {activationFiles.get(i), traceFiles.get(i)}); + setup.profiler.copyFromFiles(activationFiles.get(i), traceFiles.get(i)); + setup.profiler.processTraces(); + } + logger.log(Level.INFO, "{0}", setup.getSpans()); } - logger.info("{}", reporter.getSpans()); } } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java index 4dc80e38b..c42f38f38 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java @@ -16,33 +16,33 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; -import static org.assertj.core.api.Assertions.assertThat; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.doReturn; - -import co.elastic.apm.agent.MockReporter; -import co.elastic.apm.agent.MockTracer; -import co.elastic.apm.agent.common.util.WildcardMatcher; -import co.elastic.apm.agent.configuration.SpyConfiguration; -import co.elastic.apm.agent.impl.ElasticApmTracer; -import co.elastic.apm.agent.impl.transaction.Span; -import co.elastic.apm.agent.impl.transaction.Transaction; -import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; -import co.elastic.apm.agent.tracer.Scope; -import co.elastic.apm.agent.tracer.configuration.TimeDuration; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.verify; + +import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.data.SpanData; import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.stream.Collectors; -import javax.annotation.Nullable; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,18 +50,14 @@ import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.JRE; import org.junit.jupiter.api.condition.OS; -import org.stagemonitor.configuration.ConfigurationRegistry; +import org.mockito.Mockito; // async-profiler doesn't work on Windows @DisabledOnOs(OS.WINDOWS) @DisabledOnAppleSilicon class SamplingProfilerTest { - private MockReporter reporter; - - @Nullable private ElasticApmTracer tracer; - private SamplingProfiler profiler; - private ProfilingConfiguration profilingConfig; + private ProfilerTestSetup setup; @BeforeEach void setup() { @@ -71,10 +67,10 @@ void setup() { @AfterEach void tearDown() { - if (tracer != null) { - tracer.stop(); + if (setup != null) { + setup.close(); + setup = null; } - getProfilerTempFiles().forEach(SamplingProfilerTest::silentDeleteFile); } @@ -87,7 +83,7 @@ void shouldLazilyCreateTempFilesAndCleanThem() throws Exception { // temporary files should be created on-demand, and properly deleted afterwards setupProfiler(false); - assertThat(profiler.getProfilingSessions()) + assertThat(setup.profiler.getProfilingSessions()) .describedAs("profiler should not have any session when disabled") .isEqualTo(0); @@ -95,17 +91,24 @@ void shouldLazilyCreateTempFilesAndCleanThem() throws Exception { .describedAs("should not create a temp file when disabled") .isEmpty(); - doReturn(true).when(profilingConfig).isProfilingEnabled(); + setup.close(); + setup = null; + setupProfiler(true); - awaitProfilerStarted(profiler); + awaitProfilerStarted(setup.profiler); - assertThat(getProfilerTempFiles()).describedAs("should have created two temp files").hasSize(2); + assertThat(getProfilerTempFiles()) + .describedAs("should have created two temp files") + .hasSize(2); - profiler.stop(); + setup.close(); + setup = null; assertThat(getProfilerTempFiles()) .describedAs("should delete temp files when profiler is stopped") .isEmpty(); + + } private static List getProfilerTempFiles() { @@ -120,23 +123,34 @@ private static List getProfilerTempFiles() { } } + @Test void shouldNotDeleteProvidedFiles() throws Exception { // when an existing file is provided to the profiler, we should not delete it // unlike the temporary files that are created by profiler itself - setupProfiler(true); - profiler.stop(); + InferredSpansConfiguration defaultConfig; + try (InferredSpansProcessor profiler1 = InferredSpansProcessor.builder() + .startScheduledProfiling(false) + .build()) { + defaultConfig = profiler1.profiler.config; + } Path tempFile1 = Files.createTempFile("apm-provided", "test.bin"); Path tempFile2 = Files.createTempFile("apm-provided", "test.jfr"); - SamplingProfiler otherProfiler = - new SamplingProfiler(tracer, new FixedNanoClock(), tempFile1.toFile(), tempFile2.toFile()); + try (OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().build()) { - otherProfiler.start(tracer); - awaitProfilerStarted(otherProfiler); - otherProfiler.stop(); + SamplingProfiler otherProfiler = new SamplingProfiler( + defaultConfig, + new FixedNanoClock(), + () -> sdk.getTracer("my-tracer"), + tempFile1.toFile(), tempFile2.toFile()); + + otherProfiler.start(); + awaitProfilerStarted(otherProfiler); + otherProfiler.stop(); + } assertThat(tempFile1).exists(); assertThat(tempFile2).exists(); @@ -144,98 +158,123 @@ void shouldNotDeleteProvidedFiles() throws Exception { @Test void testStartCommand() { - setupProfiler(true); - assertThat(profiler.createStartCommand()) - .isEqualTo("start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0"); - doReturn(false).when(profilingConfig).isProfilingLoggingEnabled(); - assertThat(profiler.createStartCommand()) - .isEqualTo( - "start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0,log=none"); - doReturn(TimeDuration.of("10ms")).when(profilingConfig).getSamplingInterval(); - doReturn(14).when(profilingConfig).getAsyncProfilerSafeMode(); - assertThat(profiler.createStartCommand()) - .isEqualTo( - "start,jfr,event=wall,cstack=n,interval=10ms,filter,file=null,safemode=14,log=none"); + setupProfiler(false); + assertThat(setup.profiler.createStartCommand()).isEqualTo( + "start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0"); + + setup.close(); + setupProfiler(config -> config.startScheduledProfiling(false).profilerLoggingEnabled(false)); + assertThat(setup.profiler.createStartCommand()).isEqualTo( + "start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0,log=none"); + + setup.close(); + setupProfiler(config -> config + .startScheduledProfiling(false) + .profilerLoggingEnabled(false) + .samplingInterval(Duration.ofMillis(10)) + .asyncProfilerSafeMode(14) + ); + assertThat(setup.profiler.createStartCommand()).isEqualTo( + "start,jfr,event=wall,cstack=n,interval=10ms,filter,file=null,safemode=14,log=none"); } @Test void testProfileTransaction() throws Exception { setupProfiler(true); - awaitProfilerStarted(profiler); + awaitProfilerStarted(setup.profiler); - Transaction transaction = tracer.startRootTransaction(null).withName("transaction"); - try (Scope scope = transaction.activateInScope()) { + Tracer tracer = setup.sdk.getTracer("manual-spans"); + + boolean profilingActiveOnThread; + Span tx = tracer.spanBuilder("transaction").startSpan(); + try (Scope scope = tx.makeCurrent()) { // makes sure that the rest will be captured by another profiling session // this tests that restoring which threads to profile works Thread.sleep(600); - assertThat(profiler.isProfilingActiveOnThread(Thread.currentThread())).isTrue(); - aInferred(transaction); + profilingActiveOnThread = setup.profiler.isProfilingActiveOnThread(Thread.currentThread()); + aInferred(tracer); } finally { - transaction.end(); + tx.end(); } await() .pollDelay(10, TimeUnit.MILLISECONDS) .timeout(5000, TimeUnit.MILLISECONDS) - .untilAsserted(() -> assertThat(reporter.getSpans()).hasSize(5)); + .untilAsserted(() -> assertThat(setup.getSpans()).hasSizeGreaterThanOrEqualTo(6)); + + assertThat(profilingActiveOnThread).isTrue(); - Optional testProfileTransaction = - reporter.getSpans().stream() - .filter(s -> s.getNameAsString().equals("SamplingProfilerTest#testProfileTransaction")) - .findAny(); + Optional txData = setup.getSpans().stream() + .filter(s -> s.getName().equals("transaction")) + .findAny(); + assertThat(txData).isPresent(); + assertThat(txData.get()).hasNoParent(); + + Optional testProfileTransaction = setup.getSpans().stream() + .filter(s -> s.getName().equals("SamplingProfilerTest#testProfileTransaction")) + .findAny(); assertThat(testProfileTransaction).isPresent(); - assertThat(testProfileTransaction.get().isChildOf(transaction)).isTrue(); + assertThat(testProfileTransaction.get()).hasParent(txData.get()); - Optional inferredSpanA = - reporter.getSpans().stream() - .filter(s -> s.getNameAsString().equals("SamplingProfilerTest#aInferred")) - .findAny(); + Optional inferredSpanA = setup.getSpans().stream() + .filter(s -> s.getName().equals("SamplingProfilerTest#aInferred")).findAny(); assertThat(inferredSpanA).isPresent(); - assertThat(inferredSpanA.get().isChildOf(testProfileTransaction.get())).isTrue(); + assertThat(inferredSpanA.get()).hasParent(testProfileTransaction.get()); - Optional explicitSpanB = - reporter.getSpans().stream().filter(s -> s.getNameAsString().equals("bExplicit")).findAny(); + Optional explicitSpanB = setup.getSpans().stream() + .filter(s -> s.getName().equals("bExplicit")).findAny(); assertThat(explicitSpanB).isPresent(); - assertThat(explicitSpanB.get().isChildOf(inferredSpanA.get())).isTrue(); - - Optional inferredSpanC = - reporter.getSpans().stream() - .filter(s -> s.getNameAsString().equals("SamplingProfilerTest#cInferred")) - .findAny(); + assertThat(explicitSpanB.get()).hasParent(txData.get()); + + assertThat(inferredSpanA.get().getLinks()) + .hasSize(1) + .anySatisfy(link -> { + assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); + SpanData expectedSpan = explicitSpanB.get(); + Assertions.assertThat(link.getSpanContext().getTraceId()) + .isEqualTo(expectedSpan.getTraceId()); + Assertions.assertThat(link.getSpanContext().getSpanId()) + .isEqualTo(expectedSpan.getSpanId()); + }); + + Optional inferredSpanC = setup.getSpans().stream() + .filter(s -> s.getName().equals("SamplingProfilerTest#cInferred")).findAny(); assertThat(inferredSpanC).isPresent(); - assertThat(inferredSpanC.get().isChildOf(explicitSpanB.get())).isTrue(); + assertThat(inferredSpanC.get()).hasParent(explicitSpanB.get()); - Optional inferredSpanD = - reporter.getSpans().stream() - .filter(s -> s.getNameAsString().equals("SamplingProfilerTest#dInferred")) - .findAny(); + Optional inferredSpanD = setup.getSpans().stream() + .filter(s -> s.getName().equals("SamplingProfilerTest#dInferred")).findAny(); assertThat(inferredSpanD).isPresent(); - assertThat(inferredSpanD.get().isChildOf(inferredSpanC.get())).isTrue(); + assertThat(inferredSpanD.get()).hasParent(inferredSpanC.get()); + } + + @Test + void ensurePeriodicCleanupInvoked() throws Exception { + NanoClock mockClock = Mockito.mock(NanoClock.class); + setupProfiler(config -> config.clock(mockClock)); + awaitProfilerStarted(setup.profiler); + + Thread.sleep(600); + + verify(mockClock, atLeast(1)).periodicCleanup(); } @Test @DisabledForJreRange(max = JRE.JAVA_20) void testVirtualThreadsExcluded() throws Exception { setupProfiler(true); - awaitProfilerStarted(profiler); + awaitProfilerStarted(setup.profiler); + Tracer tracer = setup.sdk.getTracer("manual-spans"); AtomicReference profilingActive = new AtomicReference<>(); - Runnable task = - () -> { - Transaction transaction = tracer.startRootTransaction(null).withName("transaction"); - try (Scope scope = transaction.activateInScope()) { - // makes sure that the rest will be captured by another profiling session - // this tests that restoring which threads to profile works - try { - Thread.sleep(600); - } catch (Exception e) { - throw new RuntimeException(e); - } - profilingActive.set(profiler.isProfilingActiveOnThread(Thread.currentThread())); - } finally { - transaction.end(); - } - }; + Runnable task = () -> { + Span tx = tracer.spanBuilder("transaction").startSpan(); + try (Scope scope = tx.makeCurrent()) { + profilingActive.set(setup.profiler.isProfilingActiveOnThread(Thread.currentThread())); + } finally { + tx.end(); + } + }; Method startVirtualThread = Thread.class.getMethod("startVirtualThread", Runnable.class); Thread virtual = (Thread) startVirtualThread.invoke(null, task); @@ -246,34 +285,35 @@ void testVirtualThreadsExcluded() throws Exception { @Test void testPostProcessingDisabled() throws Exception { - setupProfiler(true); - doReturn(false).when(profilingConfig).isPostProcessingEnabled(); - awaitProfilerStarted(profiler); + setupProfiler(config -> config.postProcessingEnabled(false)); + awaitProfilerStarted(setup.profiler); + Tracer tracer = setup.sdk.getTracer("manual-spans"); - Transaction transaction = tracer.startRootTransaction(null).withName("transaction"); - try (Scope scope = transaction.activateInScope()) { + Span tx = tracer.spanBuilder("transaction").startSpan(); + try (Scope scope = tx.makeCurrent()) { // makes sure that the rest will be captured by another profiling session // this tests that restoring which threads to profile works Thread.sleep(600); - aInferred(transaction); + aInferred(tracer); } finally { - transaction.end(); + tx.end(); } await() .pollDelay(10, TimeUnit.MILLISECONDS) .timeout(5000, TimeUnit.MILLISECONDS) - .untilAsserted(() -> assertThat(reporter.getSpans()).hasSize(1)); + .untilAsserted(() -> assertThat(setup.getSpans()).hasSize(2)); - Optional explicitSpanB = - reporter.getSpans().stream().filter(s -> s.getNameAsString().equals("bExplicit")).findAny(); + Optional explicitSpanB = setup.getSpans().stream() + .filter(s -> s.getName().equals("bExplicit")).findAny(); assertThat(explicitSpanB).isPresent(); - assertThat(explicitSpanB.get().isChildOf(transaction)).isTrue(); + assertThat(explicitSpanB.get()) + .hasParentSpanId(tx.getSpanContext().getSpanId()); } - private void aInferred(Transaction transaction) throws Exception { - Span span = transaction.createSpan().withName("bExplicit").withType("custom"); - try (Scope spanScope = span.activateInScope()) { + private void aInferred(Tracer tracer) throws Exception { + Span span = tracer.spanBuilder("bExplicit").startSpan(); + try (Scope spanScope = span.makeCurrent()) { cInferred(); } finally { span.end(); @@ -291,21 +331,20 @@ private void dInferred() throws Exception { } private void setupProfiler(boolean enabled) { - reporter = new MockReporter(); - ConfigurationRegistry config = SpyConfiguration.createSpyConfig(); - profilingConfig = config.getConfig(ProfilingConfiguration.class); - - doReturn(List.of(WildcardMatcher.valueOf(getClass().getName()))) - .when(profilingConfig) - .getIncludedClasses(); - doReturn(enabled).when(profilingConfig).isProfilingEnabled(); - doReturn(TimeDuration.of("500ms")).when(profilingConfig).getProfilingDuration(); - doReturn(TimeDuration.of("500ms")).when(profilingConfig).getProfilingInterval(); - doReturn(TimeDuration.of("5ms")).when(profilingConfig).getSamplingInterval(); - tracer = MockTracer.createRealTracer(reporter, config); - profiler = tracer.getLifecycleListener(ProfilingFactory.class).getProfiler(); + setupProfiler(config -> config.startScheduledProfiling(enabled)); } + + private void setupProfiler(Consumer configCustomizer) { + setup = ProfilerTestSetup.create(config -> { + config.profilingDuration(Duration.ofMillis(500)) + .profilerInterval(Duration.ofMillis(500)) + .samplingInterval(Duration.ofMillis(5)); + configCustomizer.accept(config); + }); + } + + private static void awaitProfilerStarted(SamplingProfiler profiler) { // ensure profiler is initialized await() diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java index ddf7c4ed8..b55722318 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler; +package co.elastic.apm.otel.profiler; import static org.assertj.core.api.Assertions.assertThat; +import co.elastic.apm.otel.profiler.ThreadMatcher; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; @@ -31,21 +32,17 @@ class ThreadMatcherTest { @Test void testLookup() { ArrayList threads = new ArrayList<>(); - threadMatcher.forEachThread( - new ThreadMatcher.NonCapturingPredicate() { - @Override - public boolean test(Thread thread, Void state) { - return thread.getId() == Thread.currentThread().getId(); - } - }, - null, - new ThreadMatcher.NonCapturingConsumer>() { - @Override - public void accept(Thread thread, List state) { - state.add(thread); - } - }, - threads); + threadMatcher.forEachThread(new ThreadMatcher.NonCapturingPredicate() { + @Override + public boolean test(Thread thread, Void state) { + return thread.getId() == Thread.currentThread().getId(); + } + }, null, new ThreadMatcher.NonCapturingConsumer>() { + @Override + public void accept(Thread thread, List state) { + state.add(thread); + } + }, threads); assertThat(threads).isEqualTo(List.of(Thread.currentThread())); } } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java index 4f940ce67..ba36fc5d9 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.asyncprofiler; +package co.elastic.apm.otel.profiler.asyncprofiler; -import static co.elastic.apm.agent.profiler.asyncprofiler.AsyncProfiler.SAFEMODE_SYSTEM_PROPERTY_NAME; +import static co.elastic.apm.otel.profiler.asyncprofiler.AsyncProfiler.SAFEMODE_SYSTEM_PROPERTY_NAME; import static org.assertj.core.api.Assertions.assertThat; -import co.elastic.apm.agent.testutils.DisabledOnAppleSilicon; +import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; import java.io.File; import java.io.FilenameFilter; import org.junit.jupiter.api.BeforeEach; @@ -55,8 +55,8 @@ void testShouldCopyLibToSpecifiedDirectory(@TempDir File nonDefaultTempDirectory AsyncProfiler.getInstance(nonDefaultTempDirectory.getAbsolutePath(), 6); assertThat(Integer.valueOf(System.getProperty(SAFEMODE_SYSTEM_PROPERTY_NAME))).isEqualTo(6); - File[] libasyncProfilers = - nonDefaultTempDirectory.listFiles(getLibasyncProfilerFilenameFilter()); + File[] libasyncProfilers = nonDefaultTempDirectory.listFiles( + getLibasyncProfilerFilenameFilter()); assertThat(libasyncProfilers).hasSizeGreaterThanOrEqualTo(1); } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java index c8834ebac..d94b53696 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.asyncprofiler; +package co.elastic.apm.otel.profiler.asyncprofiler; import static org.assertj.core.api.Assertions.assertThat; @@ -46,8 +46,8 @@ import org.kohsuke.github.PagedIterable; /** - * This test class is disabled by default. It is used as a utility for manually upgrading async - * profiler - update the {@link #TARGET_VERSION} and run on a POSIX-compatible file system. + * This test class is disabled by default. It is used as a utility for manually upgrading async profiler - update the + * {@link #TARGET_VERSION} and run on a POSIX-compatible file system. */ @Disabled public class AsyncProfilerUpgrader { @@ -57,7 +57,11 @@ public class AsyncProfilerUpgrader { static final String COMMON_BINARY_FILE_NAME = "libasyncProfiler.so"; static final String[] USED_ARTIFACTS = { - "linux-aarch64", "linux-arm", "linux-x64", "linux-x86", "macos-x64" + "linux-aarch64", + "linux-arm", + "linux-x64", + "linux-x86", + "macos-x64" }; @Test @@ -66,31 +70,24 @@ void updateAsyncProfilerBinaries() throws Exception { GHRepository repository = github.getRepository("jvm-profiling-tools/async-profiler"); GHRelease release = repository.getReleaseByTagName("v" + TARGET_VERSION); PagedIterable releaseAssets = release.listAssets(); - Path downloadDirPath = - Files.createTempDirectory(String.format("AsyncProfiler_%s_", TARGET_VERSION)); + Path downloadDirPath = Files.createTempDirectory( + String.format("AsyncProfiler_%s_", TARGET_VERSION)); for (GHAsset releaseAsset : releaseAssets) { if (releaseAsset.getContentType().equals("application/x-gzip")) { - downloadAndReplaceBinary( - releaseAsset.getBrowserDownloadUrl(), - releaseAsset.getName(), - downloadDirPath, - releaseAsset.getSize()); + downloadAndReplaceBinary(releaseAsset.getBrowserDownloadUrl(), releaseAsset.getName(), + downloadDirPath, releaseAsset.getSize()); } } // test we are now using the right version - Path thisOsLib = - getBinariesResourceDir() - .resolve( - co.elastic.apm.agent.profiler.asyncprofiler.AsyncProfiler.getLibraryFileName() - + ".so"); + Path thisOsLib = getBinariesResourceDir().resolve( + co.elastic.apm.otel.profiler.asyncprofiler.AsyncProfiler.getLibraryFileName() + ".so"); AsyncProfiler asyncProfiler = AsyncProfiler.getInstance(thisOsLib.toString()); assertThat(asyncProfiler.getVersion()).isEqualTo(TARGET_VERSION); } - private void downloadAndReplaceBinary( - String ghAssetDownloadUrl, String ghAssetName, Path targetDownloadDir, long expectedSize) - throws Exception { + private void downloadAndReplaceBinary(String ghAssetDownloadUrl, String ghAssetName, + Path targetDownloadDir, long expectedSize) throws Exception { String artifactNamePattern = null; for (String artifact : USED_ARTIFACTS) { if (ghAssetName.contains(artifact)) { @@ -105,10 +102,10 @@ private void downloadAndReplaceBinary( } Path targetDownloadPath = targetDownloadDir.resolve(ghAssetName); System.out.println( - String.format( - "Downloading from %s into %s and extracting binary", ghAssetName, targetDownloadDir)); - Path localBinaryPath = - downloadAndExtractBinary(ghAssetDownloadUrl, targetDownloadPath, expectedSize); + String.format("Downloading from %s into %s and extracting binary", ghAssetName, + targetDownloadDir)); + Path localBinaryPath = downloadAndExtractBinary(ghAssetDownloadUrl, targetDownloadPath, + expectedSize); assertThat(localBinaryPath) .describedAs("Failed to download and extract binary file from " + ghAssetDownloadUrl) .isNotNull(); @@ -118,8 +115,8 @@ private void downloadAndReplaceBinary( } @Nullable - private Path downloadAndExtractBinary( - String ghAssetDownloadUrl, Path targetDownloadPath, long expectedSize) throws IOException { + private Path downloadAndExtractBinary(String ghAssetDownloadUrl, Path targetDownloadPath, + long expectedSize) throws IOException { System.out.println("Downloading from " + ghAssetDownloadUrl); URLConnection assetUrlConnection = new URL(ghAssetDownloadUrl).openConnection(); long actualSize; @@ -135,17 +132,16 @@ private Path extractBinaryFileFromArchive(Path assetArchivePath) throws IOExcept String archiveFileName = assetArchivePath.getFileName().toString(); if (!archiveFileName.endsWith(TAR_GZ_FILE_EXTENSION)) { throw new IllegalArgumentException( - String.format( - "Cannot extract %s - expecting a path to a %s file", - archiveFileName, TAR_GZ_FILE_EXTENSION)); + String.format("Cannot extract %s - expecting a path to a %s file", archiveFileName, + TAR_GZ_FILE_EXTENSION)); } Path assetDirPath = assetArchivePath.getParent(); if (!Files.exists(assetDirPath)) { Files.createDirectory(assetDirPath); } - String extractedDirName = - archiveFileName.substring(0, archiveFileName.length() - TAR_GZ_FILE_EXTENSION.length()); + String extractedDirName = archiveFileName.substring(0, + archiveFileName.length() - TAR_GZ_FILE_EXTENSION.length()); Path extractedDirPath = assetDirPath.resolve(extractedDirName); if (!Files.exists(extractedDirPath)) { Files.createDirectory(extractedDirPath); @@ -173,23 +169,22 @@ private Path extractBinaryFileFromArchive(Path assetArchivePath) throws IOExcept /** * Replaces an existing binary file with its downloaded counterpart. - * - *

    NOTE: when replacing the existing binary file, this method attempts to apply the current - * binary file's permissions to the one replacing it, assuming the underlying file system is - * POSIX-compatible. If this is not the case, and error will occur + *

    + * NOTE: when replacing the existing binary file, this method attempts to apply the current binary file's + * permissions to the one replacing it, assuming the underlying file system is POSIX-compatible. If this is + * not the case, and error will occur + *

    * * @param downloadedArtifact the path to the downloaded binary file * @param artifactName the name of the artifact to replace, see {@link #USED_ARTIFACTS} - * @throws Exception thrown when an error occurs while trying to replace, or when running on non - * POSIX file system + * @throws Exception thrown when an error occurs while trying to replace, or when running on non POSIX file system */ private void replaceBinary(Path downloadedArtifact, String artifactName) throws Exception { if (!downloadedArtifact.toString().contains(artifactName)) { throw new IllegalArgumentException( - String.format( - "the provided path for the downloaded artifact [%s] must " - + "be of a file containing the provided artifact name: %s", - downloadedArtifact, artifactName)); + String.format("the provided path for the downloaded artifact [%s] must " + + "be of a file containing the provided artifact name: %s", downloadedArtifact, + artifactName)); } Path binariesResourceDir = getBinariesResourceDir(); @@ -201,24 +196,20 @@ private void replaceBinary(Path downloadedArtifact, String artifactName) throws } System.out.println( String.format("Replacing %s with %s", binaryResourcePath, downloadedArtifact)); - Set posixFilePermissions = - Files.getPosixFilePermissions(binaryResourcePath); + Set posixFilePermissions = Files.getPosixFilePermissions( + binaryResourcePath); Files.move(downloadedArtifact, binaryResourcePath, StandardCopyOption.REPLACE_EXISTING); Files.setPosixFilePermissions(binaryResourcePath, posixFilePermissions); } private Path getBinariesResourceDir() throws URISyntaxException { // /apm-agent-plugins/apm-profiling-plugin/target/classes/asyncprofiler - Path asyncProfilerTestResourcePath = - Paths.get(AsyncProfilerUpgrader.class.getResource("/asyncprofiler").toURI()); + Path asyncProfilerTestResourcePath = Paths.get( + AsyncProfilerUpgrader.class.getResource("/asyncprofiler").toURI()); // /apm-agent-plugins/apm-profiling-plugin/target/classes/asyncprofiler Path pluginRootDir = asyncProfilerTestResourcePath.getParent().getParent().getParent(); - // We are looking for // - // /apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler - return pluginRootDir - .resolve("src") - .resolve("main") - .resolve("resources") + // We are looking for // /apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler + return pluginRootDir.resolve("src").resolve("main").resolve("resources") .resolve("asyncprofiler"); } } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java index c73219285..d1b4c8d55 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.agent.profiler.asyncprofiler; +package co.elastic.apm.otel.profiler.asyncprofiler; -import static co.elastic.apm.agent.common.util.WildcardMatcher.caseSensitiveMatcher; +import static co.elastic.apm.otel.profiler.config.WildcardMatcher.caseSensitiveMatcher; import static org.assertj.core.api.Assertions.assertThat; -import co.elastic.apm.agent.impl.transaction.StackFrame; +import co.elastic.apm.otel.profiler.StackFrame; import java.io.File; import java.nio.ByteBuffer; import java.nio.file.Paths; @@ -40,24 +40,23 @@ void name() throws Exception { // should trigger most edge cases in the buffer being exhausted JfrParser jfrParser = new JfrParser(ByteBuffer.allocate(113), ByteBuffer.allocate(113)); - File file = - Paths.get(JfrParserTest.class.getClassLoader().getResource("recording.jfr").toURI()) - .toFile(); + File file = Paths.get(JfrParserTest.class.getClassLoader().getResource("recording.jfr").toURI()) + .toFile(); jfrParser.parse(file, List.of(), List.of(caseSensitiveMatcher("co.elastic.apm.*"))); AtomicInteger stackTraces = new AtomicInteger(); ArrayList stackFrames = new ArrayList<>(); - jfrParser.consumeStackTraces( - (threadId, stackTraceId, nanoTime) -> { - jfrParser.resolveStackTrace(stackTraceId, true, stackFrames, MAX_STACK_DEPTH); - if (!stackFrames.isEmpty()) { - stackTraces.incrementAndGet(); - assertThat(stackFrames.get(stackFrames.size() - 1).getMethodName()) - .isEqualTo("testProfileTransaction"); - assertThat(stackFrames).hasSizeLessThanOrEqualTo(MAX_STACK_DEPTH); - } - stackFrames.clear(); - }); + jfrParser.consumeStackTraces((threadId, stackTraceId, nanoTime) -> { + jfrParser.resolveStackTrace(stackTraceId, true, stackFrames, MAX_STACK_DEPTH); + if (!stackFrames.isEmpty()) { + stackTraces.incrementAndGet(); + assertThat(stackFrames.get(stackFrames.size() - 1).getMethodName()).isEqualTo( + "testProfileTransaction"); + assertThat(stackFrames).hasSizeLessThanOrEqualTo(MAX_STACK_DEPTH); + } + stackFrames.clear(); + }); assertThat(stackTraces.get()).isEqualTo(97); } + } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java new file mode 100644 index 000000000..c6dc78405 --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java @@ -0,0 +1,20 @@ +package co.elastic.apm.otel.profiler.util; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ExtendWith(DisabledOnAppleSiliconCondition.class) +public @interface DisabledOnAppleSilicon { + + /** + * The reason this annotated test class or test method is disabled. + */ + String value() default ""; +} diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java new file mode 100644 index 000000000..44d4079c8 --- /dev/null +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java @@ -0,0 +1,29 @@ +package co.elastic.apm.otel.profiler.util; + +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; +import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; + +import java.lang.reflect.AnnotatedElement; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class DisabledOnAppleSiliconCondition implements ExecutionCondition { + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + AnnotatedElement element = context.getElement().orElse(null); + return findAnnotation(element, DisabledOnAppleSilicon.class) + .map(annotation -> isOnAppleSilicon() + ? disabled(element + " is @DisabledOnAppleSilicon", annotation.value()) + : enabled("Not running on Apple silicon")) + .orElse(enabled("@DisabledOnAppleSilicon is not present")); + } + + public boolean isOnAppleSilicon() { + String os = System.getProperty("os.name").toLowerCase(); + String arch = System.getProperty("os.arch").toLowerCase(); + return os.contains("mac") && arch.contains("aarch"); + } +} diff --git a/inferred-spans/src/test/resources/logging.properties b/inferred-spans/src/test/resources/logging.properties new file mode 100644 index 000000000..3ea2eda60 --- /dev/null +++ b/inferred-spans/src/test/resources/logging.properties @@ -0,0 +1,6 @@ +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINEST + +.level=INFO +io.opentelemetry.level=FINEST +co.elastic.level=FINEST diff --git a/settings.gradle.kts b/settings.gradle.kts index c446f0841..7496cf250 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,7 @@ include("agent") include("bootstrap") include("custom") include("instrumentation") +include("inferred-spans") include("resources") include("resources:repackaged") include("smoke-tests") From 65b70306660f650859f14cabdc428647a70a5b7e Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Thu, 23 Nov 2023 12:39:01 +0100 Subject: [PATCH 04/18] spotless --- .../elastic/apm/otel/profiler/CallTree.java | 373 +++--- .../profiler/InferredSpansConfiguration.java | 8 +- .../otel/profiler/InferredSpansProcessor.java | 50 +- .../InferredSpansProcessorBuilder.java | 157 +-- .../profiler/ProfilingActivationListener.java | 22 +- .../apm/otel/profiler/SamplingProfiler.java | 458 +++---- .../elastic/apm/otel/profiler/StackFrame.java | 22 +- .../apm/otel/profiler/ThreadMatcher.java | 12 +- .../apm/otel/profiler/TraceContext.java | 37 +- .../profiler/asyncprofiler/AsyncProfiler.java | 86 +- .../profiler/asyncprofiler/BufferedFile.java | 134 ++- .../profiler/asyncprofiler/JfrParser.java | 129 +- .../asyncprofiler/ResourceExtractionUtil.java | 78 +- .../profiler/collections/CollectionUtil.java | 43 +- .../otel/profiler/collections/Hashing.java | 23 +- .../profiler/collections/Int2IntHashMap.java | 208 +--- .../collections/Int2ObjectHashMap.java | 165 +-- .../profiler/collections/IntIntConsumer.java | 22 +- .../collections/Long2LongHashMap.java | 209 +--- .../collections/Long2ObjectHashMap.java | 166 +-- .../profiler/collections/LongHashSet.java | 165 +-- .../otel/profiler/collections/LongList.java | 20 +- .../collections/LongLongConsumer.java | 22 +- .../otel/profiler/config/WildcardMatcher.java | 173 +-- .../profiler/pooling/AbstractObjectPool.java | 5 +- .../apm/otel/profiler/pooling/Allocator.java | 18 + .../apm/otel/profiler/pooling/ObjectPool.java | 31 +- .../pooling/QueueBasedObjectPool.java | 61 +- .../apm/otel/profiler/pooling/Recyclable.java | 23 +- .../apm/otel/profiler/pooling/Resetter.java | 19 +- .../apm/otel/profiler/util/ByteUtils.java | 18 + .../apm/otel/profiler/util/HexUtils.java | 20 +- .../apm/otel/profiler/util/ThreadUtils.java | 28 +- .../otel/profiler/CallTreeSpanifyTest.java | 100 +- .../apm/otel/profiler/CallTreeTest.java | 1063 ++++++++--------- .../apm/otel/profiler/FixedNanoClock.java | 9 +- .../apm/otel/profiler/ProfilerTestSetup.java | 37 +- .../profiler/SamplingProfilerQueueTest.java | 20 +- .../otel/profiler/SamplingProfilerReplay.java | 39 +- .../otel/profiler/SamplingProfilerTest.java | 146 +-- .../apm/otel/profiler/ThreadMatcherTest.java | 27 +- .../asyncprofiler/AsyncProfilerTest.java | 4 +- .../asyncprofiler/AsyncProfilerUpgrader.java | 89 +- .../profiler/asyncprofiler/JfrParserTest.java | 27 +- .../profiler/util/DisabledOnAppleSilicon.java | 22 +- .../util/DisabledOnAppleSiliconCondition.java | 26 +- 46 files changed, 2308 insertions(+), 2306 deletions(-) diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java index 7863af919..5c6391f0c 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java @@ -18,15 +18,14 @@ */ package co.elastic.apm.otel.profiler; - import static java.util.logging.Level.FINE; import static java.util.logging.Level.WARNING; import co.elastic.apm.otel.profiler.collections.LongHashSet; -import co.elastic.apm.otel.profiler.util.HexUtils; import co.elastic.apm.otel.profiler.collections.LongList; import co.elastic.apm.otel.profiler.pooling.ObjectPool; import co.elastic.apm.otel.profiler.pooling.Recyclable; +import co.elastic.apm.otel.profiler.util.HexUtils; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; @@ -44,20 +43,19 @@ /** * Converts a sequence of stack traces into a tree structure of method calls. + * *
      *             count
      *  b b     a      4
      * aaaa ──► ├─b    1
      *          └─b    1
      * 
    - *

    - * It also stores information about which span is the parent of a particular call tree node, + * + *

    It also stores information about which span is the parent of a particular call tree node, * based on which span has been {@linkplain ElasticApmTracer#getActive() active} at that time. - *

    - *

    - * This allows to {@linkplain Root#spanify() infer spans from the call tree} which have the correct parent/child relationships - * with the regular spans. - *

    + * + *

    This allows to {@linkplain Root#spanify() infer spans from the call tree} which have the + * correct parent/child relationships with the regular spans. */ @SuppressWarnings("javadoc") public class CallTree implements Recyclable { @@ -66,41 +64,38 @@ public class CallTree implements Recyclable { static final AttributeKey IS_CHILD_ATTRIBUTE_KEY = AttributeKey.booleanKey("elastic.is_child"); - private static final Attributes CHILD_LINK_ATTRIBUTES = Attributes.builder() - .put(IS_CHILD_ATTRIBUTE_KEY, true) - .build(); + private static final Attributes CHILD_LINK_ATTRIBUTES = + Attributes.builder().put(IS_CHILD_ATTRIBUTE_KEY, true).build(); static final AttributeKey STACKTRACE_ATTRIBUTE_KEY = AttributeKey.stringKey("code.stacktrace"); - @Nullable - private CallTree parent; + @Nullable private CallTree parent; protected int count; private List children = new ArrayList<>(INITIAL_CHILD_SIZE); - @Nullable - private StackFrame frame; + @Nullable private StackFrame frame; protected long start; private long lastSeen; private boolean ended; private long activationTimestamp = -1; + /** - * The context of the transaction or span which is the direct parent of this call tree node. - * Used in {@link #spanify} to override the parent. + * The context of the transaction or span which is the direct parent of this call tree node. Used + * in {@link #spanify} to override the parent. */ - @Nullable - private TraceContext activeContextOfDirectParent; + @Nullable private TraceContext activeContextOfDirectParent; + private long deactivationTimestamp = -1; private boolean isSpan; private int depth; + /** * @see co.elastic.apm.agent.impl.transaction.AbstractSpan#childIds */ - @Nullable - private LongList childIds; - @Nullable - private LongList maybeChildIds; + @Nullable private LongList childIds; - public CallTree() { - } + @Nullable private LongList maybeChildIds; + + public CallTree() {} public void set(@Nullable CallTree parent, StackFrame frame, long nanoTime) { this.parent = parent; @@ -136,8 +131,8 @@ public void activation(TraceContext traceContext, long activationTimestamp) { this.activationTimestamp = activationTimestamp; } - protected void handleDeactivation(TraceContext deactivatedSpan, long activationTimestamp, - long deactivationTimestamp) { + protected void handleDeactivation( + TraceContext deactivatedSpan, long activationTimestamp, long deactivationTimestamp) { if (deactivatedSpan.idEquals(activeContextOfDirectParent)) { this.deactivationTimestamp = deactivationTimestamp; } else { @@ -147,7 +142,8 @@ protected void handleDeactivation(TraceContext deactivatedSpan, long activationT } } // if an actual child span is deactivated after this call tree node has ended - // it means that this node has actually ended at least at the same point, if not after, the actual span has been deactivated + // it means that this node has actually ended at least at the same point, if not after, the + // actual span has been deactivated // // [a(inferred)] ─► [a(inferred) ] ← set end timestamp to timestamp of deactivation of b // └─[b(actual) ] └─[b(actual) ] @@ -165,29 +161,38 @@ private boolean happenedAfter(long timestamp) { return lastSeen < timestamp; } - public static CallTree.Root createRoot(ObjectPool rootPool, byte[] traceContext, - long nanoTime) { + public static CallTree.Root createRoot( + ObjectPool rootPool, byte[] traceContext, long nanoTime) { CallTree.Root root = rootPool.createInstance(); root.set(traceContext, nanoTime); return root; } /** - * Adds a single stack trace to the call tree which either updates the {@link #lastSeen} timestamp of an existing call tree node, - * {@linkplain #end ends} a node, or {@linkplain #addChild adds a new child}. + * Adds a single stack trace to the call tree which either updates the {@link #lastSeen} timestamp + * of an existing call tree node, {@linkplain #end ends} a node, or {@linkplain #addChild adds a + * new child}. * * @param stackFrames the stack trace which is iterated over in reverse order * @param index the current index of {@code stackFrames} - * @param activeSpan the trace context of the currently {@linkplain ElasticApmTracer#getActive()} active transaction/span + * @param activeSpan the trace context of the currently {@linkplain ElasticApmTracer#getActive()} + * active transaction/span * @param activationTimestamp the timestamp of when {@code traceContext} has been activated * @param nanoTime the timestamp of when this stack trace has been recorded */ - protected CallTree addFrame(List stackFrames, int index, - @Nullable TraceContext activeSpan, long activationTimestamp, long nanoTime, - ObjectPool callTreePool, long minDurationNs, Root root) { + protected CallTree addFrame( + List stackFrames, + int index, + @Nullable TraceContext activeSpan, + long activationTimestamp, + long nanoTime, + ObjectPool callTreePool, + long minDurationNs, + Root root) { count++; lastSeen = nanoTime; - // c ee ← traceContext not set - they are not a child of the active span but the frame below them + // c ee ← traceContext not set - they are not a child of the active span but the frame + // below them // bbb dd ← traceContext set // ------ ← all new CallTree during this period should have the traceContext set // a aaaaaa a @@ -196,7 +201,8 @@ protected CallTree addFrame(List stackFrames, int index, // this branch is already aware of the activation // this means the provided activeSpan is not a direct parent of new child nodes - if (activeSpan != null && this.activeContextOfDirectParent != null + if (activeSpan != null + && this.activeContextOfDirectParent != null && this.activeContextOfDirectParent.idEquals(activeSpan)) { activeSpan = null; } @@ -211,16 +217,42 @@ protected CallTree addFrame(List stackFrames, int index, final StackFrame frame = stackFrames.get(--index); if (lastChild != null) { if (!lastChild.isEnded() && frame.equals(lastChild.frame)) { - topOfStack = lastChild.addFrame(stackFrames, index, activeSpan, activationTimestamp, - nanoTime, callTreePool, minDurationNs, root); + topOfStack = + lastChild.addFrame( + stackFrames, + index, + activeSpan, + activationTimestamp, + nanoTime, + callTreePool, + minDurationNs, + root); endChild = false; } else { - topOfStack = addChild(frame, stackFrames, index, activeSpan, activationTimestamp, - nanoTime, callTreePool, minDurationNs, root); + topOfStack = + addChild( + frame, + stackFrames, + index, + activeSpan, + activationTimestamp, + nanoTime, + callTreePool, + minDurationNs, + root); } } else { - topOfStack = addChild(frame, stackFrames, index, activeSpan, activationTimestamp, nanoTime, - callTreePool, minDurationNs, root); + topOfStack = + addChild( + frame, + stackFrames, + index, + activeSpan, + activationTimestamp, + nanoTime, + callTreePool, + minDurationNs, + root); } } if (lastChild != null && !lastChild.isEnded() && endChild) { @@ -231,8 +263,8 @@ protected CallTree addFrame(List stackFrames, int index, } /** - * This method is called when we know for sure that the maybe child ids are actually belonging to this call tree. - * This is the case after we've seen another frame represented by this call tree. + * This method is called when we know for sure that the maybe child ids are actually belonging to + * this call tree. This is the case after we've seen another frame represented by this call tree. * * @see #addMaybeChildId(long) */ @@ -248,17 +280,24 @@ private void transferMaybeChildIdsToChildIds() { } } - private CallTree addChild(StackFrame frame, List stackFrames, int index, - @Nullable TraceContext traceContext, long activationTimestamp, long nanoTime, - ObjectPool callTreePool, long minDurationNs, Root root) { + private CallTree addChild( + StackFrame frame, + List stackFrames, + int index, + @Nullable TraceContext traceContext, + long activationTimestamp, + long nanoTime, + ObjectPool callTreePool, + long minDurationNs, + Root root) { CallTree callTree = callTreePool.createInstance(); callTree.set(this, frame, nanoTime); if (traceContext != null) { callTree.activation(traceContext, activationTimestamp); } children.add(callTree); - return callTree.addFrame(stackFrames, index, null, activationTimestamp, nanoTime, callTreePool, - minDurationNs, root); + return callTree.addFrame( + stackFrames, index, null, activationTimestamp, nanoTime, callTreePool, minDurationNs, root); } long getDurationUs() { @@ -298,7 +337,8 @@ protected void end(ObjectPool pool, long minDurationNs, Root root) { if (parent != null) { // we know there's always exactly one activation in the parent's childIds // that needs to be transferred to this call tree node - // in the above example, 1's child id would be first transferred from a to b and then from b to c + // in the above example, 1's child id would be first transferred from a to b and then from b + // to c // this ensures that the UI knows that c is the parent of 1 parent.giveLastChildIdTo(this); } @@ -341,7 +381,8 @@ private boolean isFasterThan(long minDurationNs) { } private boolean deactivationHappenedBeforeEnd() { - return activeContextOfDirectParent != null && deactivationTimestamp > -1 + return activeContextOfDirectParent != null + && deactivationTimestamp > -1 && lastSeen > deactivationTimestamp; } @@ -393,7 +434,8 @@ private void toString(Appendable out, int level) throws IOException { out.append(frame != null ? frame.getClassName() : "null") .append('.') .append(frame != null ? frame.getMethodName() : "null") - .append(' ').append(Integer.toString(count)) + .append(' ') + .append(Integer.toString(count)) .append('\n'); for (CallTree node : children) { node.toString(out, level + 1); @@ -406,8 +448,7 @@ int spanify( TraceContext parentContext, NanoClock clock, StringBuilder tempBuilder, - Tracer tracer - ) { + Tracer tracer) { int createdSpans = 0; if (activeContextOfDirectParent != null) { parentSpan = null; @@ -421,15 +462,26 @@ int spanify( } List children = getChildren(); for (int i = 0, size = children.size(); i < size; i++) { - createdSpans += children.get(i) - .spanify(root, span != null ? span : parentSpan, parentContext, clock, tempBuilder, - tracer); + createdSpans += + children + .get(i) + .spanify( + root, + span != null ? span : parentSpan, + parentContext, + clock, + tempBuilder, + tracer); } return createdSpans; } - protected Span asSpan(Root root, @Nullable Span parentSpan, TraceContext parentContext, - Tracer tracer, NanoClock clock, + protected Span asSpan( + Root root, + @Nullable Span parentSpan, + TraceContext parentContext, + Tracer tracer, + NanoClock clock, StringBuilder tempBuilder) { Context parentOtelCtx; @@ -452,13 +504,17 @@ protected Span asSpan(Root root, @Nullable Span parentSpan, TraceContext parentC transferMaybeChildIdsToChildIds(); - SpanBuilder spanBuilder = tracer.spanBuilder(tempBuilder.toString()) - .setParent(parentOtelCtx) - .setStartTimestamp(clock.toEpochNanos(parentContext.getClockAnchor(), this.start), - TimeUnit.NANOSECONDS); + SpanBuilder spanBuilder = + tracer + .spanBuilder(tempBuilder.toString()) + .setParent(parentOtelCtx) + .setStartTimestamp( + clock.toEpochNanos(parentContext.getClockAnchor(), this.start), + TimeUnit.NANOSECONDS); insertChildIdLinks(spanBuilder, Span.fromContext(parentOtelCtx).getSpanContext(), tempBuilder); - // we're not interested in the very bottom of the stack which contains things like accepting and handling connections + // we're not interested in the very bottom of the stack which contains things like accepting and + // handling connections if (parentSpan != null || !root.rootContext.idEquals(parentContext)) { // we're never spanifying the root assert this.parent != null; @@ -468,39 +524,41 @@ protected Span asSpan(Root root, @Nullable Span parentSpan, TraceContext parentC } Span span = spanBuilder.startSpan(); - span.end(clock.toEpochNanos(parentContext.getClockAnchor(), this.start + getDurationNs()), + span.end( + clock.toEpochNanos(parentContext.getClockAnchor(), this.start + getDurationNs()), TimeUnit.NANOSECONDS); return span; } - private void insertChildIdLinks(SpanBuilder span, SpanContext parentContext, - StringBuilder tempBuilder) { + private void insertChildIdLinks( + SpanBuilder span, SpanContext parentContext, StringBuilder tempBuilder) { if (childIds == null || childIds.isEmpty()) { return; } for (int i = 0; i < childIds.getSize(); i++) { tempBuilder.setLength(0); HexUtils.appendLongAsHex(childIds.get(i), tempBuilder); - SpanContext spanContext = SpanContext.create( - parentContext.getTraceId(), - tempBuilder.toString(), - parentContext.getTraceFlags(), - parentContext.getTraceState() - ); + SpanContext spanContext = + SpanContext.create( + parentContext.getTraceId(), + tempBuilder.toString(), + parentContext.getTraceFlags(), + parentContext.getTraceState()); span.addLink(spanContext, CHILD_LINK_ATTRIBUTES); } } - /** - * Fill in the stack trace up to the parent span - */ + /** Fill in the stack trace up to the parent span */ private void fillStackTrace(StringBuilder resultBuilder) { if (parent != null && !this.isSpan) { if (resultBuilder.length() > 0) { resultBuilder.append('\n'); } - resultBuilder.append("at ") - .append(frame.getClassName()).append('.').append(frame.getMethodName()) + resultBuilder + .append("at ") + .append(frame.getClassName()) + .append('.') + .append(frame.getMethodName()) .append('('); frame.appendFileName(resultBuilder); resultBuilder.append(')'); @@ -509,10 +567,11 @@ private void fillStackTrace(StringBuilder resultBuilder) { } /** - * Recycles this subtree to the provided pool recursively. - * Note that this method ends by recycling {@code this} node (i.e. - this subtree root), which means that - * the caller of this method should make sure that no reference to this object is held anywhere. - *

    ALSO NOTE: MAKE SURE NOT TO CALL THIS METHOD FOR {@link CallTree.Root} INSTANCES.

    + * Recycles this subtree to the provided pool recursively. Note that this method ends by recycling + * {@code this} node (i.e. - this subtree root), which means that the caller of this method + * should make sure that no reference to this object is held anywhere. + * + *

    ALSO NOTE: MAKE SURE NOT TO CALL THIS METHOD FOR {@link CallTree.Root} INSTANCES. * * @param pool the pool to which all subtree nodes are to be recycled */ @@ -550,25 +609,24 @@ public void resetState() { } /** - * When a regular span is activated, - * we want it's {@link TraceContext#getId() span.id} to be added to the call tree that represents the - * {@linkplain CallTree.Root#topOfStack top of the stack} to ensure correct parent/child relationships via re-parenting (See also {@link Span#childIds}). - *

    - * However, the {@linkplain CallTree.Root#topOfStack current top of the stack} may turn out to not be the right target. - * Consider this example: - *

    + * When a regular span is activated, we want it's {@link TraceContext#getId() span.id} to be added + * to the call tree that represents the {@linkplain CallTree.Root#topOfStack top of the stack} to + * ensure correct parent/child relationships via re-parenting (See also {@link Span#childIds}). + * + *

    However, the {@linkplain CallTree.Root#topOfStack current top of the stack} may turn out to + * not be the right target. Consider this example: + * *

        * bb
        * aa aa
        *   1  1  ← activation
        * 
    - *

    - * We would add the id of span {@code 1} to {@code b}'s {@link #maybeChildIds}. - * But after seeing the next frame, - * we realize the {@code b} has already ended and that we should {@link #giveMaybeChildIdsTo} from {@code b} and give it to {@code a}. - * This logic is implemented in {@link CallTree.Root#addStackTrace}. - * After seeing another frame of {@code a}, we know that {@code 1} is really the child of {@code a}, so we {@link #transferMaybeChildIdsToChildIds()}. - *

    + * + *

    We would add the id of span {@code 1} to {@code b}'s {@link #maybeChildIds}. But after + * seeing the next frame, we realize the {@code b} has already ended and that we should {@link + * #giveMaybeChildIdsTo} from {@code b} and give it to {@code a}. This logic is implemented in + * {@link CallTree.Root#addStackTrace}. After seeing another frame of {@code a}, we know that + * {@code 1} is really the child of {@code a}, so we {@link #transferMaybeChildIdsToChildIds()}. * * @param id the child span id to add to this call tree element */ @@ -611,7 +669,6 @@ void giveChildIdsTo(CallTree giveTo) { this.childIds = null; } - void giveLastChildIdTo(CallTree giveTo) { if (childIds != null && !childIds.isEmpty()) { giveTo.addChildId(childIds.remove(childIds.getSize() - 1)); @@ -635,38 +692,38 @@ public int getDepth() { } /** - * A special kind of a {@link CallTree} node which represents the root of the call tree. - * This acts as the interface to the outside to add new nodes to the tree or to update existing ones by + * A special kind of a {@link CallTree} node which represents the root of the call tree. This acts + * as the interface to the outside to add new nodes to the tree or to update existing ones by * {@linkplain #addStackTrace adding stack traces}. */ public static class Root extends CallTree implements Recyclable { private static final Logger logger = Logger.getLogger(Root.class.getName()); private static final StackFrame ROOT_FRAME = new StackFrame("root", "root"); + /** - * The context of the thread root, - * mostly a transaction or a span which got activated in an auxiliary thread + * The context of the thread root, mostly a transaction or a span which got activated in an + * auxiliary thread */ protected TraceContext rootContext; + /** - * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() active}. - * This is lazily deserialized from {@link #activeSpanSerialized} if there's an actual {@linkplain #addStackTrace stack trace} - * for this activation. - */ - @Nullable - private TraceContext activeSpan; - /** - * The timestamp of when {@link #activeSpan} got activated + * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() + * active}. This is lazily deserialized from {@link #activeSpanSerialized} if there's an actual + * {@linkplain #addStackTrace stack trace} for this activation. */ + @Nullable private TraceContext activeSpan; + + /** The timestamp of when {@link #activeSpan} got activated */ private long activationTimestamp = -1; + /** - * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() active}, - * in its {@linkplain TraceContext#serialize serialized} form. + * The context of the transaction or span which is currently {@link ElasticApmTracer#getActive() + * active}, in its {@linkplain TraceContext#serialize serialized} form. */ private byte[] activeSpanSerialized = new byte[TraceContext.SERIALIZED_LENGTH]; - @Nullable - private CallTree previousTopOfStack; - @Nullable - private CallTree topOfStack; + + @Nullable private CallTree previousTopOfStack; + @Nullable private CallTree topOfStack; private final LongHashSet activeSet = new LongHashSet(); @@ -682,8 +739,8 @@ private void set(byte[] traceContext, long nanoTime) { public void setActiveSpan(byte[] activeSpanSerialized, long timestamp) { activationTimestamp = timestamp; - System.arraycopy(activeSpanSerialized, 0, this.activeSpanSerialized, 0, - activeSpanSerialized.length); + System.arraycopy( + activeSpanSerialized, 0, this.activeSpanSerialized, 0, activeSpanSerialized.length); this.activeSpan = null; } @@ -721,9 +778,11 @@ public void onDeactivation(byte[] deactivated, byte[] active, long timestamp) { if (activeSpan != null) { handleDeactivation(activeSpan, activationTimestamp, timestamp); } - // else: activeSpan has not been materialized because no stack traces were added during this activation + // else: activeSpan has not been materialized because no stack traces were added during this + // activation setActiveSpan(active, timestamp); - // we're not interested in tracking nested activations that happen before we see the first stack trace + // we're not interested in tracking nested activations that happen before we see the first + // stack trace // that's because isNestedActivation is only called if topOfStack != null // this optimizes for the case where we have no stack traces for a fast executing transaction if (topOfStack != null) { @@ -732,10 +791,15 @@ public void onDeactivation(byte[] deactivated, byte[] active, long timestamp) { } } - public void addStackTrace(List stackTrace, long nanoTime, - ObjectPool callTreePool, long minDurationNs) { - // only "materialize" trace context if there's actually an associated stack trace to the activation - // avoids allocating a TraceContext for very short activations which have no effect on the CallTree anyway + public void addStackTrace( + List stackTrace, + long nanoTime, + ObjectPool callTreePool, + long minDurationNs) { + // only "materialize" trace context if there's actually an associated stack trace to the + // activation + // avoids allocating a TraceContext for very short activations which have no effect on the + // CallTree anyway boolean firstFrameAfterActivation = false; if (activeSpan == null) { firstFrameAfterActivation = true; @@ -743,15 +807,27 @@ public void addStackTrace(List stackTrace, long nanoTime, activeSpan.deserialize(activeSpanSerialized); } previousTopOfStack = topOfStack; - topOfStack = addFrame(stackTrace, stackTrace.size(), activeSpan, activationTimestamp, - nanoTime, callTreePool, minDurationNs, this); - - // After adding the first frame after an activation, we can check if we added the child ids to the correct CallTree - // If the new top of stack is not a successor (a different branch vs just added nodes on the same branch) + topOfStack = + addFrame( + stackTrace, + stackTrace.size(), + activeSpan, + activationTimestamp, + nanoTime, + callTreePool, + minDurationNs, + this); + + // After adding the first frame after an activation, we can check if we added the child ids to + // the correct CallTree + // If the new top of stack is not a successor (a different branch vs just added nodes on the + // same branch) // we have to transfer the child ids of not yet deactivated spans to the new top of the stack. // See also CallTreeTest.testActivationAfterMethodEnds and following tests. - if (firstFrameAfterActivation && previousTopOfStack != topOfStack - && previousTopOfStack != null && previousTopOfStack.hasChildIds()) { + if (firstFrameAfterActivation + && previousTopOfStack != topOfStack + && previousTopOfStack != null + && previousTopOfStack.hasChildIds()) { if (!topOfStack.isSuccessor(previousTopOfStack)) { CallTree commonAncestor = findCommonAncestor(previousTopOfStack, topOfStack); CallTree newParent = commonAncestor != null ? commonAncestor : topOfStack; @@ -782,23 +858,23 @@ private CallTree findCommonAncestor(CallTree previousTopOfStack, CallTree topOfS } /** - * Creates spans for call tree nodes if they are either not a {@linkplain #isPillar() pillar} or are a {@linkplain #isLeaf() leaf}. - * Nodes which are not converted to {@link Span}s are part of the {@link Span#stackFrames} for the nodes which do get converted to a span. - *

    - * Parent/child relationships with the regular spans are maintained. - * One exception is that an inferred span can't be the parent of a regular span. - * That is because the regular spans have already been reported once the inferred spans are created. - * In the future, we might make it possible to update the {@link TraceContext#parentId} - * of a regular span so that it correctly reflects being a child of an inferred span. - *

    + * Creates spans for call tree nodes if they are either not a {@linkplain #isPillar() pillar} or + * are a {@linkplain #isLeaf() leaf}. Nodes which are not converted to {@link Span}s are part of + * the {@link Span#stackFrames} for the nodes which do get converted to a span. + * + *

    Parent/child relationships with the regular spans are maintained. One exception is that an + * inferred span can't be the parent of a regular span. That is because the regular spans have + * already been reported once the inferred spans are created. In the future, we might make it + * possible to update the {@link TraceContext#parentId} of a regular span so that it correctly + * reflects being a child of an inferred span. */ public int spanify(NanoClock clock, Tracer tracer) { StringBuilder tempBuilder = new StringBuilder(); int createdSpans = 0; List callTrees = getChildren(); for (int i = 0, size = callTrees.size(); i < size; i++) { - createdSpans += callTrees.get(i) - .spanify(this, null, rootContext, clock, tempBuilder, tracer); + createdSpans += + callTrees.get(i).spanify(this, null, rootContext, clock, tempBuilder, tracer); } return createdSpans; } @@ -807,12 +883,11 @@ public TraceContext getRootContext() { return rootContext; } - /** - * Recycles this tree to the provided pools. - * First, all child subtrees are recycled recursively to the children pool. - * Then, {@code this} root node is recycled to the root pool. This means that the caller of this method - * should make sure that no reference to this root object is held anywhere. + * Recycles this tree to the provided pools. First, all child subtrees are recycled recursively + * to the children pool. Then, {@code this} root node is recycled to the root pool. This means + * that the caller of this method should make sure that no reference to this root object is + * held anywhere. * * @param childrenPool object pool for all non-root nodes * @param rootPool object pool for root nodes diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java index de1b3178c..73f9baee0 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java @@ -47,8 +47,7 @@ public class InferredSpansConfiguration { List excludedClasses, Duration profilerInterval, Duration profilingDuration, - String profilerLibDirectory - ) { + String profilerLibDirectory) { this.profilerLoggingEnabled = profilerLoggingEnabled; this.backupDiagnosticFiles = backupDiagnosticFiles; this.asyncProfilerSafeMode = asyncProfilerSafeMode; @@ -107,13 +106,12 @@ public boolean isBackupDiagnosticFiles() { } public String getProfilerLibDirectory() { - return profilerLibDirectory == null || profilerLibDirectory.isEmpty() ? System.getProperty( - "java.io.tmpdir") + return profilerLibDirectory == null || profilerLibDirectory.isEmpty() + ? System.getProperty("java.io.tmpdir") : profilerLibDirectory; } public boolean isPostProcessingEnabled() { return postProcessingEnabled; } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java index 011911712..f438c02ea 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler; import io.opentelemetry.api.GlobalOpenTelemetry; @@ -20,7 +38,7 @@ public class InferredSpansProcessor implements SpanProcessor { public static final String TRACER_NAME = "elastic-inferred-spans"; - //Visible for testing + // Visible for testing final SamplingProfiler profiler; private Tracer tracer; @@ -30,8 +48,7 @@ public class InferredSpansProcessor implements SpanProcessor { NanoClock clock, boolean startScheduledProfiling, @Nullable File activationEventsFile, - @Nullable File jfrFile - ) { + @Nullable File jfrFile) { profiler = new SamplingProfiler(config, clock, this::getTracer, activationEventsFile, jfrFile); if (startScheduledProfiling) { profiler.start(); @@ -43,7 +60,8 @@ public static InferredSpansProcessorBuilder builder() { } /** - * @param provider the provider to use. Null means that {@link GlobalOpenTelemetry} will be used lazily. + * @param provider the provider to use. Null means that {@link GlobalOpenTelemetry} will be used + * lazily. */ public synchronized void setTracerProvider(TracerProvider provider) { tracer = provider.get(TRACER_NAME); @@ -60,8 +78,7 @@ public boolean isStartRequired() { } @Override - public void onEnd(ReadableSpan span) { - } + public void onEnd(ReadableSpan span) {} @Override public boolean isEndRequired() { @@ -72,15 +89,17 @@ public boolean isEndRequired() { public CompletableResultCode shutdown() { CompletableResultCode result = new CompletableResultCode(); logger.fine("Stopping Inferred Spans Processor"); - Executors.newSingleThreadExecutor().submit(() -> { - try { - profiler.stop(); - result.succeed(); - } catch (Exception e) { - logger.log(Level.SEVERE, "Failed to stop Inferred Spans Processor", e); - result.fail(); - } - }); + Executors.newSingleThreadExecutor() + .submit( + () -> { + try { + profiler.stop(); + result.succeed(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to stop Inferred Spans Processor", e); + result.fail(); + } + }); return result; } @@ -94,5 +113,4 @@ private Tracer getTracer() { } return tracer; } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java index 34a23fdc8..5081ff39d 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler; import co.elastic.apm.otel.profiler.config.WildcardMatcher; @@ -15,56 +33,54 @@ public class InferredSpansProcessorBuilder { private Duration samplingInterval = Duration.ofMillis(50); private Duration inferredSpansMinDuration = Duration.ZERO; private List includedClasses = WildcardMatcher.matchAllList(); - private List excludedClasses = Arrays.asList( - WildcardMatcher.caseSensitiveMatcher("java.*"), - WildcardMatcher.caseSensitiveMatcher("javax.*"), - WildcardMatcher.caseSensitiveMatcher("sun.*"), - WildcardMatcher.caseSensitiveMatcher("com.sun.*"), - WildcardMatcher.caseSensitiveMatcher("jdk.*"), - WildcardMatcher.caseSensitiveMatcher("org.apache.tomcat.*"), - WildcardMatcher.caseSensitiveMatcher("org.apache.catalina.*"), - WildcardMatcher.caseSensitiveMatcher("org.apache.coyote.*"), - WildcardMatcher.caseSensitiveMatcher("org.jboss.as.*"), - WildcardMatcher.caseSensitiveMatcher("org.glassfish.*"), - WildcardMatcher.caseSensitiveMatcher("org.eclipse.jetty.*"), - WildcardMatcher.caseSensitiveMatcher("com.ibm.websphere.*"), - WildcardMatcher.caseSensitiveMatcher("io.undertow.*") - ); + private List excludedClasses = + Arrays.asList( + WildcardMatcher.caseSensitiveMatcher("java.*"), + WildcardMatcher.caseSensitiveMatcher("javax.*"), + WildcardMatcher.caseSensitiveMatcher("sun.*"), + WildcardMatcher.caseSensitiveMatcher("com.sun.*"), + WildcardMatcher.caseSensitiveMatcher("jdk.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.tomcat.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.catalina.*"), + WildcardMatcher.caseSensitiveMatcher("org.apache.coyote.*"), + WildcardMatcher.caseSensitiveMatcher("org.jboss.as.*"), + WildcardMatcher.caseSensitiveMatcher("org.glassfish.*"), + WildcardMatcher.caseSensitiveMatcher("org.eclipse.jetty.*"), + WildcardMatcher.caseSensitiveMatcher("com.ibm.websphere.*"), + WildcardMatcher.caseSensitiveMatcher("io.undertow.*")); private Duration profilerInterval = Duration.ofSeconds(5); private Duration profilingDuration = Duration.ofSeconds(5); private String profilerLibDirectory = null; - //The following options are only intended to be modified in tests + // The following options are only intended to be modified in tests private NanoClock clock = new SpanAnchoredNanoClock(); private boolean startScheduledProfiling = true; private @Nullable File activationEventsFile = null; private @Nullable File jfrFile = null; - InferredSpansProcessorBuilder() { - - } + InferredSpansProcessorBuilder() {} public InferredSpansProcessor build() { - InferredSpansConfiguration config = new InferredSpansConfiguration( - profilerLoggingEnabled, - backupDiagnosticFiles, - asyncProfilerSafeMode, - postProcessingEnabled, - samplingInterval, - inferredSpansMinDuration, - includedClasses, - excludedClasses, - profilerInterval, - profilingDuration, - profilerLibDirectory - ); - return new InferredSpansProcessor(config, clock, startScheduledProfiling, activationEventsFile, - jfrFile); + InferredSpansConfiguration config = + new InferredSpansConfiguration( + profilerLoggingEnabled, + backupDiagnosticFiles, + asyncProfilerSafeMode, + postProcessingEnabled, + samplingInterval, + inferredSpansMinDuration, + includedClasses, + excludedClasses, + profilerInterval, + profilingDuration, + profilerLibDirectory); + return new InferredSpansProcessor( + config, clock, startScheduledProfiling, activationEventsFile, jfrFile); } /** - * By default, async profiler prints warning messages about missing JVM symbols to standard output. - * Set this option to {@code true} to suppress such messages + * By default, async profiler prints warning messages about missing JVM symbols to standard + * output. Set this option to {@code true} to suppress such messages */ public InferredSpansProcessorBuilder profilerLoggingEnabled(boolean profilerLoggingEnabled) { this.profilerLoggingEnabled = profilerLoggingEnabled; @@ -77,9 +93,10 @@ public InferredSpansProcessorBuilder backupDiagnosticFiles(boolean backupDiagnos } /** - * Can be used for analysis: the Async Profiler's area that deals with recovering stack trace frames - * is known to be sensitive in some systems. It is used as a bit mask using values are between 0 and 31, - * where 0 enables all recovery attempts and 31 disables all five (corresponding 1, 2, 4, 8 and 16). + * Can be used for analysis: the Async Profiler's area that deals with recovering stack trace + * frames is known to be sensitive in some systems. It is used as a bit mask using values are + * between 0 and 31, where 0 enables all recovery attempts and 31 disables all five (corresponding + * 1, 2, 4, 8 and 16). */ public InferredSpansProcessorBuilder asyncProfilerSafeMode(int asyncProfilerSafeMode) { this.asyncProfilerSafeMode = asyncProfilerSafeMode; @@ -87,7 +104,8 @@ public InferredSpansProcessorBuilder asyncProfilerSafeMode(int asyncProfilerSafe } /** - * Can be used to test the effect of the async-profiler in isolation from the agent's post-processing. + * Can be used to test the effect of the async-profiler in isolation from the agent's + * post-processing. */ public InferredSpansProcessorBuilder postProcessingEnabled(boolean postProcessingEnabled) { this.postProcessingEnabled = postProcessingEnabled; @@ -95,10 +113,10 @@ public InferredSpansProcessorBuilder postProcessingEnabled(boolean postProcessin } /** - * The frequency at which stack traces are gathered within a profiling session. - * The lower you set it, the more accurate the durations will be. - * This comes at the expense of higher overhead and more spans for potentially irrelevant operations. - * The minimal duration of a profiling-inferred span is the same as the value of this setting. + * The frequency at which stack traces are gathered within a profiling session. The lower you set + * it, the more accurate the durations will be. This comes at the expense of higher overhead and + * more spans for potentially irrelevant operations. The minimal duration of a profiling-inferred + * span is the same as the value of this setting. */ public InferredSpansProcessorBuilder samplingInterval(Duration samplingInterval) { this.samplingInterval = samplingInterval; @@ -106,9 +124,9 @@ public InferredSpansProcessorBuilder samplingInterval(Duration samplingInterval) } /** - * The minimum duration of an inferred span. - * Note that the min duration is also implicitly set by the sampling interval. - * However, increasing the sampling interval also decreases the accuracy of the duration of inferred spans. + * The minimum duration of an inferred span. Note that the min duration is also implicitly set by + * the sampling interval. However, increasing the sampling interval also decreases the accuracy of + * the duration of inferred spans. */ public InferredSpansProcessorBuilder inferredSpansMinDuration(Duration inferredSpansMinDuration) { this.inferredSpansMinDuration = inferredSpansMinDuration; @@ -116,39 +134,35 @@ public InferredSpansProcessorBuilder inferredSpansMinDuration(Duration inferredS } /** - * If set, the agent will only create inferred spans for methods which match this list. - * Setting a value may slightly reduce overhead and can reduce clutter by only creating spans for the classes you are interested in. - * Example: org.example.myapp.* + * If set, the agent will only create inferred spans for methods which match this list. Setting a + * value may slightly reduce overhead and can reduce clutter by only creating spans for the + * classes you are interested in. Example: org.example.myapp.* */ public InferredSpansProcessorBuilder includedClasses(List includedClasses) { this.includedClasses = includedClasses; return this; } - /** - * Excludes classes for which no profiler-inferred spans should be created. - */ + /** Excludes classes for which no profiler-inferred spans should be created. */ public InferredSpansProcessorBuilder excludedClasses(List excludedClasses) { this.excludedClasses = excludedClasses; return this; } - /** - * The interval at which profiling sessions should be started. - */ + /** The interval at which profiling sessions should be started. */ public InferredSpansProcessorBuilder profilerInterval(Duration profilerInterval) { this.profilerInterval = profilerInterval; return this; } /** - * The duration of a profiling session. - * For sampled transactions which fall within a profiling session (they start after and end before the session), - * so-called inferred spans will be created. - * They appear in the trace waterfall view like regular spans. - * NOTE: It is not recommended to set much higher durations as it may fill the activation events file and async-profiler's frame buffer. - * Warnings will be logged if the activation events file is full. - * If you want to have more profiling coverage, try decreasing {@link #profilerInterval(Duration)}. + * The duration of a profiling session. For sampled transactions which fall within a profiling + * session (they start after and end before the session), so-called inferred spans will be + * created. They appear in the trace waterfall view like regular spans. NOTE: It is not + * recommended to set much higher durations as it may fill the activation events file and + * async-profiler's frame buffer. Warnings will be logged if the activation events file is full. + * If you want to have more profiling coverage, try decreasing {@link + * #profilerInterval(Duration)}. */ public InferredSpansProcessorBuilder profilingDuration(Duration profilingDuration) { this.profilingDuration = profilingDuration; @@ -160,36 +174,27 @@ public InferredSpansProcessorBuilder profilerLibDirectory(String profilerLibDire return this; } - /** - * For testing only. - */ + /** For testing only. */ InferredSpansProcessorBuilder clock(NanoClock clock) { this.clock = clock; return this; } - /** - * For testing only. - */ + /** For testing only. */ InferredSpansProcessorBuilder startScheduledProfiling(boolean startScheduledProfiling) { this.startScheduledProfiling = startScheduledProfiling; return this; } - /** - * For testing only. - */ + /** For testing only. */ InferredSpansProcessorBuilder activationEventsFile(@Nullable File activationEventsFile) { this.activationEventsFile = activationEventsFile; return this; } - /** - * For testing only. - */ + /** For testing only. */ InferredSpansProcessorBuilder jfrFile(@Nullable File jfrFile) { this.jfrFile = jfrFile; return this; } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java index a80303740..6f2603b81 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java @@ -36,11 +36,11 @@ public class ProfilingActivationListener implements Closeable { } public static void ensureInitialized() { - //does nothing but ensures that the static initializer ran + // does nothing but ensures that the static initializer ran } - private static volatile List activeListeners = Collections.emptyList(); - + private static volatile List activeListeners = + Collections.emptyList(); private static class ContextStorageWrapper implements ContextStorage { @@ -119,25 +119,17 @@ public void close() { public void beforeActivate(Span oldContext, Span newContext) { if (newContext.getSpanContext().isValid() && newContext.getSpanContext().isSampled() - && !ThreadUtils.isVirtual(Thread.currentThread()) - ) { - profiler.onActivation( - newContext, - oldContext.getSpanContext().isValid() ? oldContext : null - ); + && !ThreadUtils.isVirtual(Thread.currentThread())) { + profiler.onActivation(newContext, oldContext.getSpanContext().isValid() ? oldContext : null); } } public void afterDeactivate(Span deactivatedContext, Span newContext) { if (deactivatedContext.getSpanContext().isValid() && deactivatedContext.getSpanContext().isSampled() - && !ThreadUtils.isVirtual(Thread.currentThread()) - ) { + && !ThreadUtils.isVirtual(Thread.currentThread())) { profiler.onDeactivation( - deactivatedContext, - newContext.getSpanContext().isValid() ? newContext : null - ); + deactivatedContext, newContext.getSpanContext().isValid() ? newContext : null); } } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java index e95c32d1d..b11a95040 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java @@ -61,54 +61,53 @@ import javax.annotation.Nullable; /** - * Correlates {@link ActivationEvent}s with {@link StackFrame}s which are recorded by {@link AsyncProfiler}, - * a native {@code AsyncGetCallTree}-based - * (and therefore non safepoint-biased) - * JVMTI agent. - *

    - * Recording of {@link ActivationEvent}s: - *

    - *

    - * The {@link #onActivation} and {@link #onDeactivation} methods are called by {@link ProfilingActivationListener} - * which register an {@link ActivationEvent} to a {@linkplain #eventBuffer ring buffer} whenever a {@link Span} - * gets {@link Span#activate()}d or {@link Span#deactivate()}d while a {@linkplain #profilingSessionOngoing profiling session is ongoing}. - * A background thread consumes the {@link ActivationEvent}s and writes them to a {@linkplain #activationEventsBuffer direct buffer} - * which is flushed to a {@linkplain #activationEventsFileChannel file}. - * That is necessary because within a profiling session (which lasts 10s by default) there may be many more {@link ActivationEvent}s - * than the ring buffer {@link #RING_BUFFER_SIZE can hold}. - * The file can hold {@link #ACTIVATION_EVENTS_IN_FILE} events and each is {@link ActivationEvent#SERIALIZED_SIZE} in size. - * This process is completely garbage free thanks to the {@link RingBuffer} acting as an object pool for {@link ActivationEvent}s. - *

    - *

    - * Recording stack traces: - *

    - *

    - * The same background thread that processes the {@link ActivationEvent}s starts the wall clock profiler of async-profiler via - * {@link AsyncProfiler#execute(String)}. - * After the {@link InferredSpansConfiguration#getProfilingDuration()} is over it stops the profiling and starts processing the JFR file created - * by async-profiler with {@link JfrParser}. - *

    - *

    - * Correlating {@link ActivationEvent}s with the traces recorded by {@link AsyncProfiler}: - *

    - *

    - * After both the JFR file and the file containing the {@link ActivationEvent}s have been written, - * it's now time to process them in tandem by correlating based on thread ids and timestamps. - * The result of this correlation, performed by {@link #processTraces}, - * are {@link CallTree}s which are created for each thread which has seen an {@linkplain Span#activate() activation} - * and at least one stack trace. - * Once {@linkplain ActivationEvent#handleDeactivationEvent(SamplingProfiler) handling the deactivation event} of the root span in a thread - * (after which {@link ElasticApmTracer#getActive()} would return {@code null}), - * the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext) converted into regular spans}. - *

    - *

    - * Overall, the allocation rate does not depend on the number of {@link ActivationEvent}s but only on - * {@link InferredSpansConfiguration#getProfilingInterval()} and {@link InferredSpansConfiguration#getSamplingInterval()}. - * Having said that, there are some optimizations so that the JFR file is not processed at all if there have not been any - * {@link ActivationEvent} in a given profiling session. - * Also, only if there's a {@link CallTree.Root} for a {@link StackTraceEvent}, - * we will {@link JfrParser#resolveStackTrace(long, boolean, List, int) resolve the full stack trace}. - *

    + * Correlates {@link ActivationEvent}s with {@link StackFrame}s which are recorded by {@link + * AsyncProfiler}, a native {@code + * AsyncGetCallTree}-based (and therefore non + * safepoint-biased) JVMTI agent. + * + *

    Recording of {@link ActivationEvent}s: + * + *

    The {@link #onActivation} and {@link #onDeactivation} methods are called by {@link + * ProfilingActivationListener} which register an {@link ActivationEvent} to a {@linkplain + * #eventBuffer ring buffer} whenever a {@link Span} gets {@link Span#activate()}d or {@link + * Span#deactivate()}d while a {@linkplain #profilingSessionOngoing profiling session is ongoing}. A + * background thread consumes the {@link ActivationEvent}s and writes them to a {@linkplain + * #activationEventsBuffer direct buffer} which is flushed to a {@linkplain + * #activationEventsFileChannel file}. That is necessary because within a profiling session (which + * lasts 10s by default) there may be many more {@link ActivationEvent}s than the ring buffer {@link + * #RING_BUFFER_SIZE can hold}. The file can hold {@link #ACTIVATION_EVENTS_IN_FILE} events and each + * is {@link ActivationEvent#SERIALIZED_SIZE} in size. This process is completely garbage free + * thanks to the {@link RingBuffer} acting as an object pool for {@link ActivationEvent}s. + * + *

    Recording stack traces: + * + *

    The same background thread that processes the {@link ActivationEvent}s starts the wall clock + * profiler of async-profiler via {@link AsyncProfiler#execute(String)}. After the {@link + * InferredSpansConfiguration#getProfilingDuration()} is over it stops the profiling and starts + * processing the JFR file created by async-profiler with {@link JfrParser}. + * + *

    Correlating {@link ActivationEvent}s with the traces recorded by {@link AsyncProfiler}: + * + *

    After both the JFR file and the file containing the {@link ActivationEvent}s have been + * written, it's now time to process them in tandem by correlating based on thread ids and + * timestamps. The result of this correlation, performed by {@link #processTraces}, are {@link + * CallTree}s which are created for each thread which has seen an {@linkplain Span#activate() + * activation} and at least one stack trace. Once {@linkplain + * ActivationEvent#handleDeactivationEvent(SamplingProfiler) handling the deactivation event} of the + * root span in a thread (after which {@link ElasticApmTracer#getActive()} would return {@code + * null}), the {@link CallTree} is {@linkplain CallTree#spanify(CallTree.Root, TraceContext) + * converted into regular spans}. + * + *

    Overall, the allocation rate does not depend on the number of {@link ActivationEvent}s but + * only on {@link InferredSpansConfiguration#getProfilingInterval()} and {@link + * InferredSpansConfiguration#getSamplingInterval()}. Having said that, there are some optimizations + * so that the JFR file is not processed at all if there have not been any {@link ActivationEvent} + * in a given profiling session. Also, only if there's a {@link CallTree.Root} for a {@link + * StackTraceEvent}, we will {@link JfrParser#resolveStackTrace(long, boolean, List, int) resolve + * the full stack trace}. */ class SamplingProfiler implements Runnable { @@ -123,25 +122,33 @@ class SamplingProfiler implements Runnable { private final EventTranslatorTwoArg ACTIVATION_EVENT_TRANSLATOR = new EventTranslatorTwoArg() { @Override - public void translateTo(ActivationEvent event, long sequence, Span active, - Span previouslyActive) { - event.activation(active, Thread.currentThread().getId(), previouslyActive, - nanoClock.nanoTime(), nanoClock); + public void translateTo( + ActivationEvent event, long sequence, Span active, Span previouslyActive) { + event.activation( + active, + Thread.currentThread().getId(), + previouslyActive, + nanoClock.nanoTime(), + nanoClock); } }; private final EventTranslatorTwoArg DEACTIVATION_EVENT_TRANSLATOR = new EventTranslatorTwoArg() { @Override - public void translateTo(ActivationEvent event, long sequence, Span active, - Span previouslyActive) { - event.deactivation(active, Thread.currentThread().getId(), previouslyActive, - nanoClock.nanoTime(), nanoClock); + public void translateTo( + ActivationEvent event, long sequence, Span active, Span previouslyActive) { + event.deactivation( + active, + Thread.currentThread().getId(), + previouslyActive, + nanoClock.nanoTime(), + nanoClock); } }; // sizeof(ActivationEvent) is 176B so the ring buffer should be around 880KiB static final int RING_BUFFER_SIZE = 4 * 1024; - //Visible for testing + // Visible for testing final InferredSpansConfiguration config; private final ScheduledExecutorService scheduler; private final Long2ObjectHashMap profiledThreads = new Long2ObjectHashMap<>(); @@ -152,24 +159,24 @@ public void translateTo(ActivationEvent event, long sequence, Span active, private final ObjectPool rootPool; private final ThreadMatcher threadMatcher = new ThreadMatcher(); private final EventPoller poller; - @Nullable - private File jfrFile; + @Nullable private File jfrFile; private boolean canDeleteJfrFile; - private final WriteActivationEventToFileHandler writeActivationEventToFileHandler = new WriteActivationEventToFileHandler(); - @Nullable - private JfrParser jfrParser; + private final WriteActivationEventToFileHandler writeActivationEventToFileHandler = + new WriteActivationEventToFileHandler(); + @Nullable private JfrParser jfrParser; private volatile int profilingSessions; private final ByteBuffer activationEventsBuffer; + /** - * Used to efficiently write {@link #activationEventsBuffer} via {@link FileChannel#write(ByteBuffer)} + * Used to efficiently write {@link #activationEventsBuffer} via {@link + * FileChannel#write(ByteBuffer)} */ - @Nullable - private File activationEventsFile; + @Nullable private File activationEventsFile; + private boolean canDeleteActivationEventsFile; - @Nullable - private FileChannel activationEventsFileChannel; + @Nullable private FileChannel activationEventsFileChannel; private final ObjectPool callTreePool; private final TraceContext contextForLogging; @@ -181,24 +188,29 @@ public void translateTo(ActivationEvent event, long sequence, Span active, /** * Creates a sampling profiler, optionally relying on existing files. - *

    - * This constructor is most likely used for tests that rely on a known set of files + * + *

    This constructor is most likely used for tests that rely on a known set of files * * @param tracer tracer * @param nanoClock clock * @param activationEventsFile activation events file, if {@literal null} a temp file will be used * @param jfrFile java flight recorder file, if {@literal null} a temp file will be used instead */ - SamplingProfiler(InferredSpansConfiguration config, NanoClock nanoClock, + SamplingProfiler( + InferredSpansConfiguration config, + NanoClock nanoClock, Supplier tracerProvider, - @Nullable File activationEventsFile, @Nullable File jfrFile) { + @Nullable File activationEventsFile, + @Nullable File jfrFile) { this.config = config; this.tracerProvider = tracerProvider; - this.scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> { - Thread result = new Thread(runnable); - result.setName("elastic-otel-inferred-spans"); - return result; - }); + this.scheduler = + Executors.newSingleThreadScheduledExecutor( + runnable -> { + Thread result = new Thread(runnable); + result.setName("elastic-otel-inferred-spans"); + return result; + }); this.nanoClock = nanoClock; this.eventBuffer = createRingBuffer(); this.sequence = new Sequence(); @@ -206,19 +218,26 @@ public void translateTo(ActivationEvent event, long sequence, Span active, this.eventBuffer.addGatingSequences(sequence); this.poller = eventBuffer.newPoller(); contextForLogging = new TraceContext(); - this.callTreePool = ObjectPool.createRecyclable(2 * 1024, new Allocator() { - @Override - public CallTree createInstance() { - return new CallTree(); - } - }); - // call tree roots are pooled so that fast activations/deactivations with no associated stack traces don't cause allocations - this.rootPool = ObjectPool.createRecyclable(512, new Allocator() { - @Override - public CallTree.Root createInstance() { - return new CallTree.Root(); - } - }); + this.callTreePool = + ObjectPool.createRecyclable( + 2 * 1024, + new Allocator() { + @Override + public CallTree createInstance() { + return new CallTree(); + } + }); + // call tree roots are pooled so that fast activations/deactivations with no associated stack + // traces don't cause allocations + this.rootPool = + ObjectPool.createRecyclable( + 512, + new Allocator() { + @Override + public CallTree.Root createInstance() { + return new CallTree.Root(); + } + }); this.jfrFile = jfrFile; activationEventsBuffer = ByteBuffer.allocateDirect(ACTIVATION_EVENTS_BUFFER_SIZE); this.activationEventsFile = activationEventsFile; @@ -226,8 +245,8 @@ public CallTree.Root createInstance() { } /** - * For testing only! - * This method must only be called in tests and some period after activation / deactivation events, as otherwise it is racy. + * For testing only! This method must only be called in tests and some period after activation / + * deactivation events, as otherwise it is racy. * * @param thread the Thread to check. * @return true, if profiling is active for the given thread. @@ -248,8 +267,9 @@ private synchronized void createFilesIfRequired() throws IOException { canDeleteActivationEventsFile = true; } if (activationEventsFileChannel == null || !activationEventsFileChannel.isOpen()) { - activationEventsFileChannel = FileChannel.open(activationEventsFile.toPath(), - StandardOpenOption.READ, StandardOpenOption.WRITE); + activationEventsFileChannel = + FileChannel.open( + activationEventsFile.toPath(), StandardOpenOption.READ, StandardOpenOption.WRITE); } if (activationEventsFileChannel.size() == 0) { preAllocate(activationEventsFileChannel, PRE_ALLOCATE_ACTIVATION_EVENTS_FILE_MB); @@ -288,23 +308,24 @@ public ActivationEvent newInstance() { /** * Called whenever a span is activated. - *

    - * This and {@link #onDeactivation} are the only methods which are executed in a multi-threaded + * + *

    This and {@link #onDeactivation} are the only methods which are executed in a multi-threaded * context. - *

    * * @param activeSpan the span which is about to be activated * @param previouslyActive the span which has previously been activated - * @return {@code true}, if the event could be processed, {@code false} if the internal event queue is full which means the event has been discarded + * @return {@code true}, if the event could be processed, {@code false} if the internal event + * queue is full which means the event has been discarded */ public boolean onActivation(Span activeSpan, @Nullable Span previouslyActive) { if (profilingSessionOngoing) { if (previouslyActive == null) { - AsyncProfiler.getInstance(config.getProfilerLibDirectory(), - config.getAsyncProfilerSafeMode()).enableProfilingCurrentThread(); + AsyncProfiler.getInstance( + config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()) + .enableProfilingCurrentThread(); } - boolean success = eventBuffer.tryPublishEvent(ACTIVATION_EVENT_TRANSLATOR, activeSpan, - previouslyActive); + boolean success = + eventBuffer.tryPublishEvent(ACTIVATION_EVENT_TRANSLATOR, activeSpan, previouslyActive); if (!success) { logger.fine("Could not add activation event to ring buffer as no slots are available"); } @@ -315,23 +336,24 @@ public boolean onActivation(Span activeSpan, @Nullable Span previouslyActive) { /** * Called whenever a span is deactivated. - *

    - * This and {@link #onActivation} are the only methods which are executed in a multi-threaded + * + *

    This and {@link #onActivation} are the only methods which are executed in a multi-threaded * context. - *

    * * @param activeSpan the span which is about to be activated * @param previouslyActive the span which has previously been activated - * @return {@code true}, if the event could be processed, {@code false} if the internal event queue is full which means the event has been discarded + * @return {@code true}, if the event could be processed, {@code false} if the internal event + * queue is full which means the event has been discarded */ public boolean onDeactivation(Span activeSpan, @Nullable Span previouslyActive) { if (profilingSessionOngoing) { if (previouslyActive == null) { - AsyncProfiler.getInstance(config.getProfilerLibDirectory(), - config.getAsyncProfilerSafeMode()).disableProfilingCurrentThread(); + AsyncProfiler.getInstance( + config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()) + .disableProfilingCurrentThread(); } - boolean success = eventBuffer.tryPublishEvent(DEACTIVATION_EVENT_TRANSLATOR, activeSpan, - previouslyActive); + boolean success = + eventBuffer.tryPublishEvent(DEACTIVATION_EVENT_TRANSLATOR, activeSpan, previouslyActive); if (!success) { logger.fine("Could not add deactivation event to ring buffer as no slots are available"); } @@ -382,8 +404,9 @@ public void run() { } private void profile(Duration profilingDuration) throws Exception { - AsyncProfiler asyncProfiler = AsyncProfiler.getInstance(config.getProfilerLibDirectory(), - config.getAsyncProfilerSafeMode()); + AsyncProfiler asyncProfiler = + AsyncProfiler.getInstance( + config.getProfilerLibDirectory(), config.getAsyncProfilerSafeMode()); try { String startCommand = createStartCommand(); String startMessage = asyncProfiler.execute(startCommand); @@ -395,15 +418,18 @@ private void profile(Duration profilingDuration) throws Exception { //noinspection NonAtomicOperationOnVolatileField profilingSessions++; - // When post-processing is disabled activation events are ignored, but we still need to invoke this method - // as it is the one enforcing the sampling session duration. As a side effect it will also consume + // When post-processing is disabled activation events are ignored, but we still need to invoke + // this method + // as it is the one enforcing the sampling session duration. As a side effect it will also + // consume // residual activation events if post-processing is disabled dynamically consumeActivationEventsFromRingBufferAndWriteToFile(profilingDuration); String stopMessage = asyncProfiler.execute("stop"); logger.fine(stopMessage); - // When post-processing is disabled, jfr file will not be parsed and the heavy processing will not occur + // When post-processing is disabled, jfr file will not be parsed and the heavy processing will + // not occur // as this method aborts when no activation events are buffered processTraces(); } catch (InterruptedException | ClosedByInterruptException e) { @@ -416,10 +442,13 @@ private void profile(Duration profilingDuration) throws Exception { } String createStartCommand() { - StringBuilder startCommand = new StringBuilder("start,jfr,event=wall,cstack=n,interval=") - .append(config.getSamplingInterval().toMillis()).append("ms,filter,file=") - .append(jfrFile) - .append(",safemode=").append(config.getAsyncProfilerSafeMode()); + StringBuilder startCommand = + new StringBuilder("start,jfr,event=wall,cstack=n,interval=") + .append(config.getSamplingInterval().toMillis()) + .append("ms,filter,file=") + .append(jfrFile) + .append(",safemode=") + .append(config.getAsyncProfilerSafeMode()); if (!config.isProfilingLoggingEnabled()) { startCommand.append(",log=none"); } @@ -427,8 +456,8 @@ String createStartCommand() { } /** - * When doing continuous profiling (interval=duration), - * we have to tell async-profiler which threads it should profile after re-starting it. + * When doing continuous profiling (interval=duration), we have to tell async-profiler which + * threads it should profile after re-starting it. */ private void restoreFilterState(AsyncProfiler asyncProfiler) { threadMatcher.forEachThread( @@ -445,8 +474,7 @@ public void accept(Thread thread, AsyncProfiler asyncProfiler) { asyncProfiler.enableProfilingThread(thread); } }, - asyncProfiler - ); + asyncProfiler); } private void consumeActivationEventsFromRingBufferAndWriteToFile(Duration profilingDuration) @@ -520,13 +548,15 @@ public void processTraces() throws IOException { "Max stack depth reached. Set profiling_included_classes or profiling_excluded_classes."); } // stack frames may not contain any Java frames - // see https://github.com/jvm-profiling-tools/async-profiler/issues/271#issuecomment-582430233 + // see + // https://github.com/jvm-profiling-tools/async-profiler/issues/271#issuecomment-582430233 if (!stackFrames.isEmpty()) { try { - root.addStackTrace(stackFrames, stackTrace.nanoTime, callTreePool, - inferredSpansMinDuration); + root.addStackTrace( + stackFrames, stackTrace.nanoTime, callTreePool, inferredSpansMinDuration); } catch (Exception e) { - logger.log(Level.WARNING, + logger.log( + Level.WARNING, "Removing call tree for thread {0} because of exception while adding a stack trace: {1} {2}", new Object[] {stackTrace.threadId, e.getClass(), e.getMessage()}); logger.log(Level.FINE, e.getMessage(), e); @@ -553,9 +583,11 @@ private void backupDiagnosticFiles(long eof) throws IOException { Path profilerDir = Paths.get(System.getProperty("java.io.tmpdir"), "profiler"); profilerDir.toFile().mkdir(); - try (FileChannel activationsFile = FileChannel.open( - profilerDir.resolve(now + "-activations.dat"), StandardOpenOption.CREATE_NEW, - StandardOpenOption.WRITE)) { + try (FileChannel activationsFile = + FileChannel.open( + profilerDir.resolve(now + "-activations.dat"), + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE)) { if (eof > 0) { activationEventsFileChannel.transferTo(0, eof, activationsFile); } else { @@ -572,21 +604,23 @@ private long getInferredSpansMinDurationNs() { } /** - * Returns stack trace events of relevant threads sorted by timestamp. - * The events in the JFR file are not in order. - * Even for the same thread, a more recent event might come before an older event. - * In order to be able to correlate stack trace events and activation events, both need to be in order. - *

    - * Returns only events for threads where at least one activation happened (because only those are profiled by async-profiler) + * Returns stack trace events of relevant threads sorted by timestamp. The events in the JFR file + * are not in order. Even for the same thread, a more recent event might come before an older + * event. In order to be able to correlate stack trace events and activation events, both need to + * be in order. + * + *

    Returns only events for threads where at least one activation happened (because only those + * are profiled by async-profiler) */ private List getSortedStackTraceEvents(JfrParser jfrParser) throws IOException { final List stackTraceEvents = new ArrayList<>(); - jfrParser.consumeStackTraces(new JfrParser.StackTraceConsumer() { - @Override - public void onCallTree(long threadId, long stackTraceId, long nanoTime) { - stackTraceEvents.add(new StackTraceEvent(nanoTime, stackTraceId, threadId)); - } - }); + jfrParser.consumeStackTraces( + new JfrParser.StackTraceConsumer() { + @Override + public void onCallTree(long threadId, long stackTraceId, long nanoTime) { + stackTraceEvents.add(new StackTraceEvent(nanoTime, stackTraceId, threadId)); + } + }); Collections.sort(stackTraceEvents); return stackTraceEvents; } @@ -606,7 +640,8 @@ public void processActivationEventsUpTo(long timestamp, ActivationEvent event, l } long eventTimestamp = peekLong(buf); if (eventTimestamp < previousTimestamp && logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, + logger.log( + Level.FINE, "Timestamp of current activation event ({0}) is lower than the one from the previous event ({1})", new Object[] {eventTimestamp, previousTimestamp}); } @@ -616,7 +651,8 @@ public void processActivationEventsUpTo(long timestamp, ActivationEvent event, l try { event.handle(this); } catch (Exception e) { - logger.log(Level.WARNING, + logger.log( + Level.WARNING, "Removing call tree for thread {0} because of exception while handling activation event: {1} {2}", new Object[] {event.threadId, e.getClass(), e.getMessage()}); logger.log(Level.FINE, e.getMessage(), e); @@ -628,8 +664,8 @@ public void processActivationEventsUpTo(long timestamp, ActivationEvent event, l } } - private void readActivationEventsToBuffer(FileChannel activationEventsFileChannel, long eof, - ByteBuffer byteBuffer) throws IOException { + private void readActivationEventsToBuffer( + FileChannel activationEventsFileChannel, long eof, ByteBuffer byteBuffer) throws IOException { Buffer buf = byteBuffer; buf.clear(); long remaining = eof - activationEventsFileChannel.position(); @@ -681,21 +717,19 @@ void copyFromFiles(Path activationEvents, Path traces) throws IOException { createFilesIfRequired(); FileChannel otherActivationsChannel = FileChannel.open(activationEvents, READ); - activationEventsFileChannel.transferFrom(otherActivationsChannel, 0, - otherActivationsChannel.size()); + activationEventsFileChannel.transferFrom( + otherActivationsChannel, 0, otherActivationsChannel.size()); activationEventsFileChannel.position(otherActivationsChannel.size()); FileChannel otherTracesChannel = FileChannel.open(traces, READ); FileChannel.open(jfrFile.toPath(), WRITE) .transferFrom(otherTracesChannel, 0, otherTracesChannel.size()); } - public void start() { scheduler.scheduleAtFixedRate(nanoClock::periodicCleanup, 500, 500, TimeUnit.MILLISECONDS); scheduler.submit(this); } - public void stop() throws Exception { // cancels/interrupts the profiling thread // implicitly clears profiled threads @@ -740,13 +774,14 @@ CallTree.Root getRoot() { void clear() throws IOException { // consume all remaining events from the ring buffer try { - poller.poll(new EventPoller.Handler() { - @Override - public boolean onEvent(ActivationEvent event, long sequence, boolean endOfBatch) { - SamplingProfiler.this.sequence.set(sequence); - return true; - } - }); + poller.poll( + new EventPoller.Handler() { + @Override + public boolean onEvent(ActivationEvent event, long sequence, boolean endOfBatch) { + SamplingProfiler.this.sequence.set(sequence); + return true; + } + }); } catch (Exception e) { throw new RuntimeException(e); } @@ -795,11 +830,16 @@ public int compareTo(StackTraceEvent o) { private static class ActivationEvent { public static final int SERIALIZED_SIZE = - Long.SIZE / Byte.SIZE + // timestamp - TraceContext.SERIALIZED_LENGTH + // traceContextBuffer - TraceContext.SERIALIZED_LENGTH + // previousContextBuffer - 1 + // rootContext - Long.SIZE / Byte.SIZE + // threadId + Long.SIZE / Byte.SIZE + + // timestamp + TraceContext.SERIALIZED_LENGTH + + // traceContextBuffer + TraceContext.SERIALIZED_LENGTH + + // previousContextBuffer + 1 + + // rootContext + Long.SIZE / Byte.SIZE + + // threadId 1; // activation private long timestamp; @@ -809,31 +849,40 @@ private static class ActivationEvent { private long threadId; private boolean activation; - public void activation(Span context, long threadId, - @Nullable Span previousContext, long nanoTime, NanoClock clock) { + public void activation( + Span context, + long threadId, + @Nullable Span previousContext, + long nanoTime, + NanoClock clock) { set(context, threadId, true, previousContext, nanoTime, clock); } - public void deactivation(Span context, long threadId, - @Nullable Span previousContext, long nanoTime, NanoClock clock) { + public void deactivation( + Span context, + long threadId, + @Nullable Span previousContext, + long nanoTime, + NanoClock clock) { set(context, threadId, false, previousContext, nanoTime, clock); } - private void set(Span traceContext, long threadId, boolean activation, - @Nullable Span previousContext, long nanoTime, NanoClock clock) { + private void set( + Span traceContext, + long threadId, + boolean activation, + @Nullable Span previousContext, + long nanoTime, + NanoClock clock) { TraceContext.serialize( - traceContext.getSpanContext(), - clock.getAnchor(traceContext), - traceContextBuffer - ); + traceContext.getSpanContext(), clock.getAnchor(traceContext), traceContextBuffer); this.threadId = threadId; this.activation = activation; if (previousContext != null) { TraceContext.serialize( previousContext.getSpanContext(), clock.getAnchor(previousContext), - previousContextBuffer - ); + previousContextBuffer); rootContext = false; } else { rootContext = true; @@ -843,9 +892,10 @@ private void set(Span traceContext, long threadId, boolean activation, public void handle(SamplingProfiler samplingProfiler) { if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Handling event timestamp={0} root={1} threadId={2} activation={3}", - new Object[] {timestamp, - rootContext, threadId, activation}); + logger.log( + Level.FINE, + "Handling event timestamp={0} root={1} threadId={2} activation={3}", + new Object[] {timestamp, rootContext, threadId, activation}); } if (activation) { handleActivationEvent(samplingProfiler); @@ -865,7 +915,8 @@ private void handleActivationEvent(SamplingProfiler samplingProfiler) { } root.onActivation(traceContextBuffer, timestamp); } else if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, + logger.log( + Level.FINE, "Illegal state when handling activation event for thread {0}: no root found for this thread", threadId); } @@ -873,17 +924,20 @@ private void handleActivationEvent(SamplingProfiler samplingProfiler) { } private void startProfiling(SamplingProfiler samplingProfiler) { - CallTree.Root root = CallTree.createRoot(samplingProfiler.rootPool, traceContextBuffer, - timestamp); + CallTree.Root root = + CallTree.createRoot(samplingProfiler.rootPool, traceContextBuffer, timestamp); if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "Create call tree ({0}) for thread {1}", + logger.log( + Level.FINE, + "Create call tree ({0}) for thread {1}", new Object[] {deserialize(samplingProfiler, traceContextBuffer), threadId}); } CallTree.Root orphaned = samplingProfiler.profiledThreads.put(threadId, root); if (orphaned != null) { if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, + logger.log( + Level.FINE, "Illegal state when stopping profiling for thread {0}: orphaned root", threadId); } @@ -907,7 +961,8 @@ private void handleDeactivationEvent(SamplingProfiler samplingProfiler) { } root.onDeactivation(traceContextBuffer, previousContextBuffer, timestamp); } else if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, + logger.log( + Level.FINE, "Illegal state when handling deactivation event for thread {0}: no root found for this thread", threadId); } @@ -918,23 +973,28 @@ private void stopProfiling(SamplingProfiler samplingProfiler) { CallTree.Root callTree = samplingProfiler.profiledThreads.get(threadId); if (callTree != null && callTree.getRootContext().traceIdAndIdEquals(traceContextBuffer)) { if (logger.isLoggable(Level.FINE)) { - logger.log(Level.FINE, "End call tree ({0}) for thread {1}", + logger.log( + Level.FINE, + "End call tree ({0}) for thread {1}", new Object[] {deserialize(samplingProfiler, traceContextBuffer), threadId}); } samplingProfiler.profiledThreads.remove(threadId); try { - callTree.end(samplingProfiler.callTreePool, - samplingProfiler.getInferredSpansMinDurationNs()); - int createdSpans = callTree.spanify(samplingProfiler.getClock(), - samplingProfiler.tracerProvider.get()); + callTree.end( + samplingProfiler.callTreePool, samplingProfiler.getInferredSpansMinDurationNs()); + int createdSpans = + callTree.spanify(samplingProfiler.getClock(), samplingProfiler.tracerProvider.get()); if (logger.isLoggable(Level.FINE)) { if (createdSpans > 0) { - logger.log(Level.FINE, "Created spans ({0}) for thread {1}", + logger.log( + Level.FINE, + "Created spans ({0}) for thread {1}", new Object[] {createdSpans, threadId}); } else { - logger.log(Level.FINE, "Created no spans for thread {0} (count={1})", - new Object[] {threadId, - callTree.getCount()}); + logger.log( + Level.FINE, + "Created no spans for thread {0} (count={1})", + new Object[] {threadId, callTree.getCount()}); } } } finally { @@ -960,28 +1020,26 @@ public void deserialize(ByteBuffer buf) { threadId = buf.getLong(); activation = buf.get() == 1; } - } /** - * Does not wait but immediately returns the highest sequence which is available for read - * We never want to wait until new elements are available, - * we just want to process all available events + * Does not wait but immediately returns the highest sequence which is available for read We never + * want to wait until new elements are available, we just want to process all available events */ private static class NoWaitStrategy implements WaitStrategy { @Override - public long waitFor(long sequence, Sequence cursor, Sequence dependentSequence, - SequenceBarrier barrier) { + public long waitFor( + long sequence, Sequence cursor, Sequence dependentSequence, SequenceBarrier barrier) { return dependentSequence.get(); } @Override - public void signalAllWhenBlocking() { - } + public void signalAllWhenBlocking() {} } - // extracting to a class instead of instantiating an anonymous inner class makes a huge difference in allocations + // extracting to a class instead of instantiating an anonymous inner class makes a huge difference + // in allocations private class WriteActivationEventToFileHandler implements EventPoller.Handler { @Override public boolean onEvent(ActivationEvent event, long sequence, boolean endOfBatch) diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java index 0eea90220..7097a08bd 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java @@ -1,11 +1,28 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler; import java.util.Objects; import javax.annotation.Nullable; public class StackFrame { - @Nullable - private final String className; + @Nullable private final String className; private final String methodName; public static StackFrame of(@Nullable String className, String methodName) { @@ -82,5 +99,4 @@ public String toString() { } return className + '.' + methodName; } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java index 2a5a614db..c6bbc8bf0 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java @@ -31,16 +31,19 @@ public ThreadMatcher() { systemThreadGroup = threadGroup; } - public void forEachThread(NonCapturingPredicate predicate, S1 state1, - NonCapturingConsumer consumer, S2 state2) { + public void forEachThread( + NonCapturingPredicate predicate, + S1 state1, + NonCapturingConsumer consumer, + S2 state2) { int count = systemThreadGroup.activeCount(); do { int expectedArrayLength = count + (count / 2) + 1; if (threads.length < expectedArrayLength) { - threads = new Thread[expectedArrayLength]; //slightly grow the array size + threads = new Thread[expectedArrayLength]; // slightly grow the array size } count = systemThreadGroup.enumerate(threads, true); - //return value of enumerate() must be strictly less than the array size according to javadoc + // return value of enumerate() must be strictly less than the array size according to javadoc } while (count >= threads.length); for (int i = 0; i < count; i++) { @@ -59,5 +62,4 @@ interface NonCapturingPredicate { interface NonCapturingConsumer { void accept(T t, S state); } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java index 855e57823..fb5355178 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java @@ -1,8 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler; +import co.elastic.apm.otel.profiler.pooling.Recyclable; import co.elastic.apm.otel.profiler.util.ByteUtils; import co.elastic.apm.otel.profiler.util.HexUtils; -import co.elastic.apm.otel.profiler.pooling.Recyclable; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; @@ -10,9 +28,9 @@ import javax.annotation.Nullable; /** - * A mutable (and therefore recyclable) class storing the relevant bits of {@link SpanContext} - * for generating inferred spans. Also stores a clock-anchor for the corresponding span obtained - * via {@link NanoClock#getAnchor(Span)}. + * A mutable (and therefore recyclable) class storing the relevant bits of {@link SpanContext} for + * generating inferred spans. Also stores a clock-anchor for the corresponding span obtained via + * {@link NanoClock#getAnchor(Span)}. */ public class TraceContext implements Recyclable { @@ -24,8 +42,7 @@ public class TraceContext implements Recyclable { private long clockAnchor; - public TraceContext() { - } + public TraceContext() {} // For testing only static TraceContext fromSpanContextWithZeroClockAnchor(SpanContext ctx) { @@ -40,7 +57,6 @@ private void filLFromSpanContext(SpanContext ctx) { traceIdHigh = HexUtils.hexToLong(ctx.getTraceId(), 0); traceIdLow = HexUtils.hexToLong(ctx.getTraceId(), 16); flags = ctx.getTraceFlags().asByte(); - } public SpanContext toOtelSpanContext(StringBuilder temporaryBuilder) { @@ -54,11 +70,7 @@ public SpanContext toOtelSpanContext(StringBuilder temporaryBuilder) { String idStr = temporaryBuilder.toString(); return SpanContext.create( - traceIdStr, - idStr, - TraceFlags.fromByte(flags), - TraceState.getDefault() - ); + traceIdStr, idStr, TraceFlags.fromByte(flags), TraceState.getDefault()); } public boolean idEquals(@Nullable TraceContext o) { @@ -80,7 +92,6 @@ public void deserialize(byte[] serialized) { clockAnchor = ByteUtils.getLong(serialized, 25); } - public boolean traceIdAndIdEquals(byte[] otherSerialized) { long otherTraceIdLow = ByteUtils.getLong(otherSerialized, 0); if (otherTraceIdLow != traceIdLow) { diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java index 9b81cdc6f..3d87f9411 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java @@ -16,21 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2018 Andrei Pangin - * - * 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 co.elastic.apm.otel.profiler.asyncprofiler; import java.io.IOException; @@ -39,25 +24,22 @@ import javax.annotation.Nullable; /** - * Java API for in-process profiling. Serves as a wrapper around - * async-profiler native library. This class is a singleton. - * The first call to {@link #getInstance(String, int)} initiates loading of + * Java API for in-process profiling. Serves as a wrapper around async-profiler native library. This + * class is a singleton. The first call to {@link #getInstance(String, int)} initiates loading of * libasyncProfiler.so. - *

    - * This is based on https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/java/one/profiler/AsyncProfiler.java, - * under Apache License 2.0. - * It is modified to allow it to be shaded into the {@code co.elastic.apm} namespace - *

    + * + *

    This is based on + * https://github.com/jvm-profiling-tools/async-profiler/blob/master/src/java/one/profiler/AsyncProfiler.java, + * under Apache License 2.0. It is modified to allow it to be shaded into the {@code co.elastic.apm} + * namespace */ public class AsyncProfiler { public static final String SAFEMODE_SYSTEM_PROPERTY_NAME = "AsyncProfiler.safemode"; - @Nullable - private static volatile AsyncProfiler instance; + @Nullable private static volatile AsyncProfiler instance; - private AsyncProfiler() { - } + private AsyncProfiler() {} public static AsyncProfiler getInstance(String profilerLibDirectory, int safemode) { AsyncProfiler result = AsyncProfiler.instance; @@ -68,23 +50,30 @@ public static AsyncProfiler getInstance(String profilerLibDirectory, int safemod if (instance == null) { if (System.getProperty("java.vm.name").contains("J9")) { throw new IllegalStateException( - "OpenJ9 JVMs are not supported by async profiler. Please set " + - "profiling_inferred_spans_enabled to false"); + "OpenJ9 JVMs are not supported by async profiler. Please set " + + "profiling_inferred_spans_enabled to false"); } try { - // set the AsyncProfiler.safemode system property with the configured safemode, so that optimizations - // can be applied already at load time. Specifically, if (safemode & 14) == 14 (2, 4 and 8 bits are set), then - // async profiler will avoid enabling CompiledMethodLoad events at load time, so to workaround a relatd JVM bug - // (https://bugs.openjdk.java.net/browse/JDK-8202883, https://bugs.openjdk.java.net/browse/JDK-8173361 and friends). - // safemode can still be set for each profiling session, but it can only be stricter than the safemode + // set the AsyncProfiler.safemode system property with the configured safemode, so that + // optimizations + // can be applied already at load time. Specifically, if (safemode & 14) == 14 (2, 4 and 8 + // bits are set), then + // async profiler will avoid enabling CompiledMethodLoad events at load time, so to + // workaround a relatd JVM bug + // (https://bugs.openjdk.java.net/browse/JDK-8202883, + // https://bugs.openjdk.java.net/browse/JDK-8173361 and friends). + // safemode can still be set for each profiling session, but it can only be stricter than + // the safemode // configured at load time. System.setProperty(SAFEMODE_SYSTEM_PROPERTY_NAME, String.valueOf(safemode)); loadNativeLibrary(profilerLibDirectory); } catch (UnsatisfiedLinkError e) { - throw new IllegalStateException(String.format( - "It is likely that %s is not an executable location. Consider setting " + - "the profiling_inferred_spans_lib_directory property to a directory on a partition that allows execution", - profilerLibDirectory), e); + throw new IllegalStateException( + String.format( + "It is likely that %s is not an executable location. Consider setting " + + "the profiling_inferred_spans_lib_directory property to a directory on a partition that allows execution", + profilerLibDirectory), + e); } instance = new AsyncProfiler(); @@ -101,8 +90,12 @@ static void reset() { private static void loadNativeLibrary(String libraryDirectory) { String libraryName = getLibraryFileName(); - Path file = ResourceExtractionUtil.extractResourceToDirectory( - "asyncprofiler/" + libraryName + ".so", libraryName, ".so", Paths.get(libraryDirectory)); + Path file = + ResourceExtractionUtil.extractResourceToDirectory( + "asyncprofiler/" + libraryName + ".so", + libraryName, + ".so", + Paths.get(libraryDirectory)); System.load(file.toString()); } @@ -142,8 +135,8 @@ public void stop() throws IllegalStateException { } /** - * Execute an agent-compatible profiling command - - * the comma-separated list of arguments described in arguments.cpp + * Execute an agent-compatible profiling command - the comma-separated list of arguments described + * in arguments.cpp * * @param command Profiling command * @return The command result @@ -174,16 +167,12 @@ public void disableProfilingThread(Thread thread) throws IllegalStateException { filterThread(thread, false); } - /** - * Adds the current thread to the set of profiled threads - */ + /** Adds the current thread to the set of profiled threads */ public void enableProfilingCurrentThread() { filterThread0(null, true); } - /** - * Removes the current thread to the set of profiled threads - */ + /** Removes the current thread to the set of profiled threads */ public void disableProfilingCurrentThread() throws IllegalStateException { filterThread0(null, false); } @@ -208,5 +197,4 @@ private native void start0(String event, long interval, boolean reset) private native String execute0(String command) throws IllegalArgumentException, IOException; private native void filterThread0(Thread thread, boolean enable); - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java index 490ce7c64..17ea69edb 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java @@ -18,7 +18,6 @@ */ package co.elastic.apm.otel.profiler.asyncprofiler; - import co.elastic.apm.otel.profiler.pooling.Recyclable; import java.io.File; import java.io.IOException; @@ -30,25 +29,22 @@ import javax.annotation.Nullable; /** - * An abstraction similar to {@link MappedByteBuffer} that allows to read the content of a file with an API that is similar to - * {@link ByteBuffer}. - *

    - * Instances of this class hold a reusable buffer that contains a subset of the file, - * or the whole file if the buffer's capacity is greater or equal to the file's size. - *

    - *

    - * Whenever calling a method like {@link #getLong()} or {@link #position(long)} would exceed the currently buffered range - * the same buffer is filled with a different range of the file. - *

    - *

    - * The downside of {@link MappedByteBuffer} (and the reason for implementing this abstraction) - * is that calling methods like {@link MappedByteBuffer#get()} can increase time-to-safepoint. - * This is because these methods are implemented as JVM intrinsics. - * When the JVM executes an intrinsic, it does not switch to the native execution context which means that it's not ready to enter a safepoint - * whenever a intrinsic runs. - * As reading a file from disk can get stuck (for example when the disk is busy) calling {@link MappedByteBuffer#get()} may take a while to execute. - * While it's executing other threads have to wait for it to finish if the JVM wants to reach a safe point. - *

    + * An abstraction similar to {@link MappedByteBuffer} that allows to read the content of a file with + * an API that is similar to {@link ByteBuffer}. + * + *

    Instances of this class hold a reusable buffer that contains a subset of the file, or the + * whole file if the buffer's capacity is greater or equal to the file's size. + * + *

    Whenever calling a method like {@link #getLong()} or {@link #position(long)} would exceed the + * currently buffered range the same buffer is filled with a different range of the file. + * + *

    The downside of {@link MappedByteBuffer} (and the reason for implementing this abstraction) is + * that calling methods like {@link MappedByteBuffer#get()} can increase time-to-safepoint. This is + * because these methods are implemented as JVM intrinsics. When the JVM executes an intrinsic, it + * does not switch to the native execution context which means that it's not ready to enter a + * safepoint whenever a intrinsic runs. As reading a file from disk can get stuck (for example when + * the disk is busy) calling {@link MappedByteBuffer#get()} may take a while to execute. While it's + * executing other threads have to wait for it to finish if the JVM wants to reach a safe point. */ class BufferedFile implements Recyclable { @@ -59,18 +55,17 @@ class BufferedFile implements Recyclable { private ByteBuffer buffer; private final ByteBuffer bigBuffer; private final ByteBuffer smallBuffer; - /** - * The offset of the file from where the {@link #buffer} starts - */ + + /** The offset of the file from where the {@link #buffer} starts */ private long offset; + private boolean wholeFileInBuffer; - @Nullable - private FileChannel fileChannel; + @Nullable private FileChannel fileChannel; /** * @param bigBuffer the buffer to be used to read the whole file if the file fits into it - * @param smallBuffer the buffer to be used to read chunks of the file in case the file is larger than bigBuffer. - * Constantly seeking a file with a large buffer is very bad for performance. + * @param smallBuffer the buffer to be used to read chunks of the file in case the file is larger + * than bigBuffer. Constantly seeking a file with a large buffer is very bad for performance. */ public BufferedFile(ByteBuffer bigBuffer, ByteBuffer smallBuffer) { this.bigBuffer = bigBuffer; @@ -78,7 +73,8 @@ public BufferedFile(ByteBuffer bigBuffer, ByteBuffer smallBuffer) { } /** - * Sets the file and depending on it's size, may read the file into the {@linkplain #buffer buffer} + * Sets the file and depending on it's size, may read the file into the {@linkplain #buffer + * buffer} * * @param file the file to read from * @throws IOException If some I/O error occurs @@ -136,7 +132,8 @@ public void position(long pos) { /** * Ensures that the provided number of bytes are available in the {@linkplain #buffer buffer} * - * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain #buffer buffer} + * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain + * #buffer buffer} * @throws IOException If some I/O error occurs * @throws IllegalStateException If minRemaining is greater than the buffer's capacity */ @@ -147,8 +144,10 @@ public void ensureRemaining(int minRemaining) throws IOException { /** * Ensures that the provided number of bytes are available in the {@linkplain #buffer buffer} * - * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain #buffer buffer} - * @param maxRead the max number of bytes to read from the file in case the buffer does currently not hold {@code minRemaining} bytes + * @param minRemaining the number of bytes which are guaranteed to be available in the {@linkplain + * #buffer buffer} + * @param maxRead the max number of bytes to read from the file in case the buffer does currently + * not hold {@code minRemaining} bytes * @throws IOException If some I/O error occurs * @throws IllegalStateException If minRemaining is greater than the buffer's capacity */ @@ -158,8 +157,8 @@ public void ensureRemaining(int minRemaining, int maxRead) throws IOException { } if (minRemaining > buffer.capacity()) { throw new IllegalStateException( - String.format("Length (%d) greater than buffer capacity (%d)", minRemaining, - buffer.capacity())); + String.format( + "Length (%d) greater than buffer capacity (%d)", minRemaining, buffer.capacity())); } if (buffer.remaining() < minRemaining) { read(position(), maxRead); @@ -167,8 +166,9 @@ public void ensureRemaining(int minRemaining, int maxRead) throws IOException { } /** - * Gets a byte from the current {@linkplain #position() position} of this file. - * If the {@linkplain #buffer buffer} does not fully contain this byte, loads another slice of the file into the buffer. + * Gets a byte from the current {@linkplain #position() position} of this file. If the {@linkplain + * #buffer buffer} does not fully contain this byte, loads another slice of the file into the + * buffer. * * @return The byte at the file's current position * @throws IOException If some I/O error occurs @@ -179,8 +179,9 @@ public short get() throws IOException { } /** - * Gets a short from the current {@linkplain #position() position} of this file. - * If the {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file into the buffer. + * Gets a short from the current {@linkplain #position() position} of this file. If the + * {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file + * into the buffer. * * @return The short at the file's current position * @throws IOException If some I/O error occurs @@ -191,8 +192,9 @@ public short getShort() throws IOException { } /** - * Gets a short from the current {@linkplain #position() position} of this file. - * If the {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file into the buffer. + * Gets a short from the current {@linkplain #position() position} of this file. If the + * {@linkplain #buffer buffer} does not fully contain this short, loads another slice of the file + * into the buffer. * * @return The short at the file's current position * @throws IOException If some I/O error occurs @@ -202,8 +204,9 @@ public int getUnsignedShort() throws IOException { } /** - * Gets a int from the current {@linkplain #position() position} of this file and converts it to an unsigned short. - * If the {@linkplain #buffer buffer} does not fully contain this int, loads another slice of the file into the buffer. + * Gets a int from the current {@linkplain #position() position} of this file and converts it to + * an unsigned short. If the {@linkplain #buffer buffer} does not fully contain this int, loads + * another slice of the file into the buffer. * * @return The int at the file's current position * @throws IOException If some I/O error occurs @@ -214,8 +217,9 @@ public int getInt() throws IOException { } /** - * Gets a long from the current {@linkplain #position() position} of this file. - * If the {@linkplain #buffer buffer} does not fully contain this long, loads another slice of the file into the buffer. + * Gets a long from the current {@linkplain #position() position} of this file. If the {@linkplain + * #buffer buffer} does not fully contain this long, loads another slice of the file into the + * buffer. * * @return The long at the file's current position * @throws IOException If some I/O error occurs @@ -226,52 +230,56 @@ public long getLong() throws IOException { } /** - * Gets a byte from the underlying buffer without checking if this part of the file is actually in the buffer. - *

    - * Always mare sure to call {@link #ensureRemaining} before. - *

    + * Gets a byte from the underlying buffer without checking if this part of the file is actually in + * the buffer. + * + *

    Always mare sure to call {@link #ensureRemaining} before. * * @return The byte at the file's current position - * @throws java.nio.BufferUnderflowException If the buffer's current position is not smaller than its limit + * @throws java.nio.BufferUnderflowException If the buffer's current position is not smaller than + * its limit */ public byte getUnsafe() { return buffer.get(); } /** - * Gets a short from the underlying buffer without checking if this part of the file is actually in the buffer. - *

    - * Always mare sure to call {@link #ensureRemaining} before. - *

    + * Gets a short from the underlying buffer without checking if this part of the file is actually + * in the buffer. + * + *

    Always mare sure to call {@link #ensureRemaining} before. * * @return The byte at the file's current position - * @throws java.nio.BufferUnderflowException If there are fewer than two bytes remaining in this buffer + * @throws java.nio.BufferUnderflowException If there are fewer than two bytes remaining in this + * buffer */ public short getUnsafeShort() { return buffer.getShort(); } /** - * Gets an int from the underlying buffer without checking if this part of the file is actually in the buffer. - *

    - * Always mare sure to call {@link #ensureRemaining} before. - *

    + * Gets an int from the underlying buffer without checking if this part of the file is actually in + * the buffer. + * + *

    Always mare sure to call {@link #ensureRemaining} before. * * @return The byte at the file's current position - * @throws java.nio.BufferUnderflowException If there are fewer than four bytes remaining in this buffer + * @throws java.nio.BufferUnderflowException If there are fewer than four bytes remaining in this + * buffer */ public int getUnsafeInt() { return buffer.getInt(); } /** - * Gets a long from the underlying buffer without checking if this part of the file is actually in the buffer. - *

    - * Always mare sure to call {@link #ensureRemaining} before. - *

    + * Gets a long from the underlying buffer without checking if this part of the file is actually in + * the buffer. + * + *

    Always mare sure to call {@link #ensureRemaining} before. * * @return The byte at the file's current position - * @throws java.nio.BufferUnderflowException If there are fewer than eight bytes remaining in this buffer + * @throws java.nio.BufferUnderflowException If there are fewer than eight bytes remaining in this + * buffer */ public long getUnsafeLong() { return buffer.getLong(); diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java index 08c3a0824..e3c7ba710 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java @@ -19,10 +19,10 @@ package co.elastic.apm.otel.profiler.asyncprofiler; import co.elastic.apm.otel.profiler.StackFrame; +import co.elastic.apm.otel.profiler.collections.Int2IntHashMap; import co.elastic.apm.otel.profiler.collections.Int2ObjectHashMap; import co.elastic.apm.otel.profiler.collections.Long2LongHashMap; import co.elastic.apm.otel.profiler.collections.Long2ObjectHashMap; -import co.elastic.apm.otel.profiler.collections.Int2IntHashMap; import co.elastic.apm.otel.profiler.config.WildcardMatcher; import co.elastic.apm.otel.profiler.pooling.Recyclable; import java.io.File; @@ -38,27 +38,26 @@ import javax.annotation.Nullable; /** - * Parses the binary JFR file created by async-profiler. - * May not work with JFR files created by an actual flight recorder. - *

    - * The implementation is tuned with to minimize allocations when parsing a JFR file. - * Most data structures can be reused by first {@linkplain #resetState() resetting the state} and then {@linkplain #parse(File, List, List) parsing} - * another file. - *

    + * Parses the binary JFR file created by async-profiler. May not work with JFR files created by an + * actual flight recorder. + * + *

    The implementation is tuned with to minimize allocations when parsing a JFR file. Most data + * structures can be reused by first {@linkplain #resetState() resetting the state} and then + * {@linkplain #parse(File, List, List) parsing} another file. */ public class JfrParser implements Recyclable { private static final Logger logger = Logger.getLogger(JfrParser.class.getName()); private static final byte[] MAGIC_BYTES = new byte[] {'F', 'L', 'R', '\0'}; - private static final Set JAVA_FRAME_TYPES = new HashSet<>( - Arrays.asList("Interpreted", "JIT compiled", "Inlined")); + private static final Set JAVA_FRAME_TYPES = + new HashSet<>(Arrays.asList("Interpreted", "JIT compiled", "Inlined")); private static final int BIG_FILE_BUFFER_SIZE = 5 * 1024 * 1024; private static final int SMALL_FILE_BUFFER_SIZE = 4 * 1024; private static final String SYMBOL_EXCLUDED = "3x cluded"; private static final String SYMBOL_NULL = "n u11"; - private final static StackFrame FRAME_EXCLUDED = new StackFrame("excluded", "excluded"); - private final static StackFrame FRAME_NULL = new StackFrame("null", "null"); + private static final StackFrame FRAME_EXCLUDED = new StackFrame("excluded", "excluded"); + private static final StackFrame FRAME_NULL = new StackFrame("null", "null"); private final BufferedFile bufferedFile; private final Int2IntHashMap classIdToClassNameSymbolId = new Int2IntHashMap(-1); @@ -66,22 +65,21 @@ public class JfrParser implements Recyclable { private final Int2ObjectHashMap symbolIdToString = new Int2ObjectHashMap(); private final Int2IntHashMap stackTraceIdToFilePositions = new Int2IntHashMap(-1); private final Long2LongHashMap nativeTidToJavaTid = new Long2LongHashMap(-1); - private final Long2ObjectHashMap frameIdToFrame = new Long2ObjectHashMap(); + private final Long2ObjectHashMap frameIdToFrame = + new Long2ObjectHashMap(); private final Long2LongHashMap frameIdToMethodSymbol = new Long2LongHashMap(-1); private final Long2LongHashMap frameIdToClassId = new Long2LongHashMap(-1); // used to resolve a symbol with minimal allocations private final StringBuilder symbolBuilder = new StringBuilder(); private long eventsOffset; private long metadataOffset; - @Nullable - private boolean[] isJavaFrameType; - @Nullable - private List excludedClasses; - @Nullable - private List includedClasses; + @Nullable private boolean[] isJavaFrameType; + @Nullable private List excludedClasses; + @Nullable private List includedClasses; public JfrParser() { - this(ByteBuffer.allocateDirect(BIG_FILE_BUFFER_SIZE), + this( + ByteBuffer.allocateDirect(BIG_FILE_BUFFER_SIZE), ByteBuffer.allocateDirect(SMALL_FILE_BUFFER_SIZE)); } @@ -90,23 +88,27 @@ public JfrParser() { } /** - * Initializes the parser to make it ready for {@link #resolveStackTrace(long, boolean, List, int)} to be called. + * Initializes the parser to make it ready for {@link #resolveStackTrace(long, boolean, List, + * int)} to be called. * * @param file the JFR file to parse - * @param excludedClasses Class names to exclude in stack traces (has an effect on {@link #resolveStackTrace(long, boolean, List, int)}) - * @param includedClasses Class names to include in stack traces (has an effect on {@link #resolveStackTrace(long, boolean, List, int)}) + * @param excludedClasses Class names to exclude in stack traces (has an effect on {@link + * #resolveStackTrace(long, boolean, List, int)}) + * @param includedClasses Class names to include in stack traces (has an effect on {@link + * #resolveStackTrace(long, boolean, List, int)}) * @throws IOException if some I/O error occurs */ - public void parse(File file, List excludedClasses, - List includedClasses) throws IOException { + public void parse( + File file, List excludedClasses, List includedClasses) + throws IOException { this.excludedClasses = excludedClasses; this.includedClasses = includedClasses; bufferedFile.setFile(file); long fileSize = bufferedFile.size(); if (fileSize < 16) { throw new IllegalStateException( - "Unexpected sampling profiler error, everything else should work as expected. " + - "Please report to us with as many details, including OS and JVM details."); + "Unexpected sampling profiler error, everything else should work as expected. " + + "Please report to us with as many details, including OS and JVM details."); } if (logger.isLoggable(Level.FINE)) { logger.log(Level.FINE, "Parsing {0} ({1} bytes)", new Object[] {file, fileSize}); @@ -149,7 +151,7 @@ private void expectEventType(int expectedEventType) throws IOException { private void parseCheckpoint(long checkpointOffset) throws IOException { bufferedFile.position(checkpointOffset); - int size = bufferedFile.getInt();// size + int size = bufferedFile.getInt(); // size expectEventType(EventTypeId.EVENT_CHECKPOINT); bufferedFile.getLong(); // stop timestamp bufferedFile.getLong(); // previous checkpoint - always 0 in async-profiler @@ -205,7 +207,8 @@ private void parseContent() throws IOException { // classId is an incrementing integer, no way there are more than 2 billion distinct ones int classId = (int) bufferedFile.getUnsafeLong(); bufferedFile.getUnsafeLong(); // loader class - // symbol ids are incrementing integers, no way there are more than 2 billion distinct ones + // symbol ids are incrementing integers, no way there are more than 2 billion distinct + // ones int classNameSymbolId = (int) bufferedFile.getUnsafeLong(); classIdToClassNameSymbolId.put(classId, classNameSymbolId); // class name bufferedFile.getUnsafeShort(); // access flags @@ -217,7 +220,8 @@ private void parseContent() throws IOException { long id = bufferedFile.getUnsafeLong(); // classId is an incrementing integer, no way there are more than 2 billion distinct ones int classId = (int) bufferedFile.getUnsafeLong(); - // symbol ids are incrementing integers, no way there are more than 2 billion distinct ones + // symbol ids are incrementing integers, no way there are more than 2 billion distinct + // ones int methodNameSymbolId = (int) bufferedFile.getUnsafeLong(); frameIdToFrame.put(id, FRAME_NULL); frameIdToClassId.put(id, classId); @@ -229,7 +233,8 @@ private void parseContent() throws IOException { break; case ContentTypeId.CONTENT_SYMBOL: for (int i = 0; i < count; i++) { - // symbol ids are incrementing integers, no way there are more than 2 billion distinct ones + // symbol ids are incrementing integers, no way there are more than 2 billion distinct + // ones int symbolId = (int) bufferedFile.getLong(); int pos = (int) bufferedFile.position(); symbolIdToPos.put(symbolId, pos); @@ -300,27 +305,30 @@ public void consumeStackTraces(StackTraceConsumer callback) throws IOException { /** * Resolves the stack trace with the given {@code stackTraceId}. - *

    - * Note that his allocates strings for symbols in case a stack frame has not already been resolved for the current JFR file yet. - * These strings are currently not cached so this can create some GC pressure. - *

    - *

    - * Excludes frames based on the {@link WildcardMatcher}s supplied to {@link #parse(File, List, List)}. - *

    * - * @param stackTraceId The id of the stack traced. - * Used to look up the position of the file in which the given stack trace is stored via {@link #stackTraceIdToFilePositions}. - * @param onlyJavaFrames If {@code true}, will only resolve {@code Interpreted}, {@code JIT compiled} and {@code Inlined} frames. - * If {@code false}, will also resolve {@code Native}, {@code Kernel} and {@code C++} frames. - * @param stackFrames The mutable list where the stack frames are written to. - * Don't forget to {@link List#clear()} the list before calling this method if the list is reused. - * @param maxStackDepth The max size of the stackFrames list (excluded frames don't take up space). - * In contrast to async-profiler's {@code jstackdepth} argument this does not truncate the bottom of the stack, only the top. - * This is important to properly create a call tree without making it overly complex. + *

    Note that his allocates strings for symbols in case a stack frame has not already been + * resolved for the current JFR file yet. These strings are currently not cached so this can + * create some GC pressure. + * + *

    Excludes frames based on the {@link WildcardMatcher}s supplied to {@link #parse(File, List, + * List)}. + * + * @param stackTraceId The id of the stack traced. Used to look up the position of the file in + * which the given stack trace is stored via {@link #stackTraceIdToFilePositions}. + * @param onlyJavaFrames If {@code true}, will only resolve {@code Interpreted}, {@code JIT + * compiled} and {@code Inlined} frames. If {@code false}, will also resolve {@code Native}, + * {@code Kernel} and {@code C++} frames. + * @param stackFrames The mutable list where the stack frames are written to. Don't forget to + * {@link List#clear()} the list before calling this method if the list is reused. + * @param maxStackDepth The max size of the stackFrames list (excluded frames don't take up + * space). In contrast to async-profiler's {@code jstackdepth} argument this does not truncate + * the bottom of the stack, only the top. This is important to properly create a call tree + * without making it overly complex. * @throws IOException if there is an error reading in current buffer */ - public void resolveStackTrace(long stackTraceId, boolean onlyJavaFrames, - List stackFrames, int maxStackDepth) throws IOException { + public void resolveStackTrace( + long stackTraceId, boolean onlyJavaFrames, List stackFrames, int maxStackDepth) + throws IOException { if (!bufferedFile.isSet()) { throw new IllegalStateException("getStackTrace was called before parse"); } @@ -344,8 +352,9 @@ public void resolveStackTrace(long stackTraceId, boolean onlyJavaFrames, bufferedFile.position(position); } - private void addFrameIfIncluded(List stackFrames, boolean onlyJavaFrames, - long frameId, byte frameType) throws IOException { + private void addFrameIfIncluded( + List stackFrames, boolean onlyJavaFrames, long frameId, byte frameType) + throws IOException { if (!onlyJavaFrames || isJavaFrameType(frameType)) { StackFrame stackFrame = resolveStackFrame(frameId); if (stackFrame != FRAME_EXCLUDED) { @@ -385,8 +394,8 @@ private StringBuilder resolveSymbolBuilder(int pos, boolean replaceSlashWithDot) } private boolean isClassIncluded(CharSequence className) { - return WildcardMatcher.isAnyMatch(includedClasses, className) && WildcardMatcher.isNoneMatch( - excludedClasses, className); + return WildcardMatcher.isAnyMatch(includedClasses, className) + && WildcardMatcher.isNoneMatch(excludedClasses, className); } private StackFrame resolveStackFrame(long frameId) throws IOException { @@ -394,8 +403,8 @@ private StackFrame resolveStackFrame(long frameId) throws IOException { if (stackFrame != FRAME_NULL) { return stackFrame; } - String className = resolveSymbol( - classIdToClassNameSymbolId.get((int) frameIdToClassId.get(frameId)), true); + String className = + resolveSymbol(classIdToClassNameSymbolId.get((int) frameIdToClassId.get(frameId)), true); if (className == SYMBOL_EXCLUDED) { stackFrame = FRAME_EXCLUDED; } else { @@ -447,10 +456,12 @@ public void resetState() { public interface StackTraceConsumer { /** - * @param threadId The {@linkplain Thread#getId() Java thread id} for with the event was recorded. - * @param stackTraceId The id of the stack trace event. - * Can be used to resolve the stack trace via {@link #resolveStackTrace(long, boolean, List, int)} - * @param nanoTime The timestamp of the event which can be correlated with {@link System#nanoTime()} + * @param threadId The {@linkplain Thread#getId() Java thread id} for with the event was + * recorded. + * @param stackTraceId The id of the stack trace event. Can be used to resolve the stack trace + * via {@link #resolveStackTrace(long, boolean, List, int)} + * @param nanoTime The timestamp of the event which can be correlated with {@link + * System#nanoTime()} * @throws IOException if there is any error reading stack trace */ void onCallTree(long threadId, long stackTraceId, long nanoTime) throws IOException; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java index 270982c2e..9a845ec17 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java @@ -1,6 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.asyncprofiler; - import static java.nio.file.LinkOption.NOFOLLOW_LINKS; import static java.nio.file.StandardOpenOption.CREATE_NEW; import static java.nio.file.StandardOpenOption.READ; @@ -29,60 +46,71 @@ public class ResourceExtractionUtil { /** - * Extracts a classpath resource to {@code ${System.getProperty("java.io.tmpdir")}/$prefix-$hash.$suffix}. - * If the file has already been extracted it will not be extracted again. + * Extracts a classpath resource to {@code + * ${System.getProperty("java.io.tmpdir")}/$prefix-$hash.$suffix}. If the file has already been + * extracted it will not be extracted again. * * @param resource The classpath resource to extract. * @param prefix The prefix of the extracted file. * @param suffix The suffix of the extracted file. * @return the extracted file. */ - public static synchronized Path extractResourceToTempDirectory(String resource, String prefix, - String suffix) { - return extractResourceToDirectory(resource, prefix, suffix, - Paths.get(System.getProperty("java.io.tmpdir"))); + public static synchronized Path extractResourceToTempDirectory( + String resource, String prefix, String suffix) { + return extractResourceToDirectory( + resource, prefix, suffix, Paths.get(System.getProperty("java.io.tmpdir"))); } /** - * Extracts a classpath resource to {@code $directory/$prefix-$userHash-$hash.$suffix}. - * If the file has already been extracted it will not be extracted again. + * Extracts a classpath resource to {@code $directory/$prefix-$userHash-$hash.$suffix}. If the + * file has already been extracted it will not be extracted again. * * @param resource The classpath resource to extract. * @param prefix The prefix of the extracted file. * @param suffix The suffix of the extracted file. - * @param directory The directory in which the file is to be created, or null if the default temporary-file directory is to be used. + * @param directory The directory in which the file is to be created, or null if the default + * temporary-file directory is to be used. * @return the extracted file. */ /* * Why it's synchronized : if the same JVM try to lock file, we got an java.nio.channels.OverlappingFileLockException. * So we need to block until the file is totally written. */ - public static synchronized Path extractResourceToDirectory(String resource, String prefix, - String suffix, Path directory) { - try (InputStream resourceStream = ResourceExtractionUtil.class.getResourceAsStream( - "/" + resource)) { + public static synchronized Path extractResourceToDirectory( + String resource, String prefix, String suffix, Path directory) { + try (InputStream resourceStream = + ResourceExtractionUtil.class.getResourceAsStream("/" + resource)) { if (resourceStream == null) { throw new IllegalStateException(resource + " not found"); } UserPrincipal currentUserPrincipal = getCurrentUserPrincipal(); // we have to include current user name as multiple copies of the same agent could be attached - // to multiple JVMs, each running under a different user. Hashing makes the name path-friendly. + // to multiple JVMs, each running under a different user. Hashing makes the name + // path-friendly. String userHash = hash(currentUserPrincipal.getName()); // to guard against re-using previous versions String resourceHash = hash(ResourceExtractionUtil.class.getResourceAsStream("/" + resource)); - Path tempFile = directory.resolve( - prefix + "-" + userHash.substring(0, 32) + "-" + resourceHash.substring(0, 32) + suffix); + Path tempFile = + directory.resolve( + prefix + + "-" + + userHash.substring(0, 32) + + "-" + + resourceHash.substring(0, 32) + + suffix); try { FileAttribute[] attr; if (tempFile.getFileSystem().supportedFileAttributeViews().contains("posix")) { - attr = new FileAttribute[] { - PosixFilePermissions.asFileAttribute(EnumSet.of(OWNER_WRITE, OWNER_READ))}; + attr = + new FileAttribute[] { + PosixFilePermissions.asFileAttribute(EnumSet.of(OWNER_WRITE, OWNER_READ)) + }; } else { attr = new FileAttribute[0]; } - try (FileChannel channel = FileChannel.open(tempFile, EnumSet.of(CREATE_NEW, WRITE), - attr)) { + try (FileChannel channel = + FileChannel.open(tempFile, EnumSet.of(CREATE_NEW, WRITE), attr)) { // make other JVM instances wait until fully written try (FileLock writeLock = channel.lock()) { channel.transferFrom(Channels.newChannel(resourceStream), 0, Long.MAX_VALUE); @@ -98,7 +126,10 @@ public static synchronized Path extractResourceToDirectory(String resource, Stri "Invalid checksum of " + tempFile + ". Please delete this file."); } else if (!Files.getOwner(tempFile).equals(currentUserPrincipal)) { throw new IllegalStateException( - "File " + tempFile + " is not owned by '" + currentUserPrincipal.getName() + "File " + + tempFile + + " is not owned by '" + + currentUserPrincipal.getName() + "'. Please delete this file."); } } @@ -125,8 +156,7 @@ private static String hash(InputStream resourceAsStream) MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] buffer = new byte[1024]; DigestInputStream dis = new DigestInputStream(is, md); - while (dis.read(buffer) != -1) { - } + while (dis.read(buffer) != -1) {} return new BigInteger(1, md.digest()).toString(16); } } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java index ab18889d3..e8a90ee21 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java @@ -16,31 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; -/** - * Utility functions for collection objects. - */ +/** Utility functions for collection objects. */ public class CollectionUtil { /** * Validate that a load factor is in the range of 0.1 to 0.9. - *

    - * Load factors in the range 0.5 - 0.7 are recommended for open-addressing with linear probing. + * + *

    Load factors in the range 0.5 - 0.7 are recommended for open-addressing with linear probing. * * @param loadFactor to be validated. */ @@ -53,11 +36,11 @@ public static void validateLoadFactor(final float loadFactor) { /** * Fast method of finding the next power of 2 greater than or equal to the supplied value. - *

    - * If the value is <= 0 then 1 will be returned. - *

    - * This method is not suitable for {@link Integer#MIN_VALUE} or numbers greater than 2^30. When provided - * then {@link Integer#MIN_VALUE} will be returned. + * + *

    If the value is <= 0 then 1 will be returned. + * + *

    This method is not suitable for {@link Integer#MIN_VALUE} or numbers greater than 2^30. When + * provided then {@link Integer#MIN_VALUE} will be returned. * * @param value from which to search for next power of 2. * @return The next power of 2 or the value itself if it is a power of 2. @@ -68,11 +51,11 @@ public static int findNextPositivePowerOfTwo(final int value) { /** * Fast method of finding the next power of 2 greater than or equal to the supplied value. - *

    - * If the value is <= 0 then 1 will be returned. - *

    - * This method is not suitable for {@link Long#MIN_VALUE} or numbers greater than 2^62. When provided - * then {@link Long#MIN_VALUE} will be returned. + * + *

    If the value is <= 0 then 1 will be returned. + * + *

    This method is not suitable for {@link Long#MIN_VALUE} or numbers greater than 2^62. When + * provided then {@link Long#MIN_VALUE} will be returned. * * @param value from which to search for next power of 2. * @return The next power of 2 or the value itself if it is a power of 2. diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java index 451f8a883..2c1589501 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java @@ -16,30 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; -/** - * Hashing functions for applying to integers. - */ +/** Hashing functions for applying to integers. */ public class Hashing { - /** - * Default load factor to be used in open addressing hashed data structures. - */ + /** Default load factor to be used in open addressing hashed data structures. */ public static final float DEFAULT_LOAD_FACTOR = 0.55f; /** diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java index cdab3491f..1935fcd11 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java @@ -16,21 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; import java.io.Serializable; @@ -42,12 +27,7 @@ import java.util.NoSuchElementException; import java.util.Objects; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; - - -/** - * A open addressing with linear probing hash map specialised for primitive key and value pairs. - */ +/** A open addressing with linear probing hash map specialised for primitive key and value pairs. */ public class Int2IntHashMap implements Map, Serializable { static final int MIN_CAPACITY = 8; @@ -66,10 +46,7 @@ public Int2IntHashMap(final int missingValue) { this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, missingValue); } - public Int2IntHashMap( - final int initialCapacity, - final float loadFactor, - final int missingValue) { + public Int2IntHashMap(final int initialCapacity, final float loadFactor, final int missingValue) { this(initialCapacity, loadFactor, missingValue, true); } @@ -121,8 +98,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. - * This is a function of the current capacity and load factor. + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -130,16 +107,12 @@ public int resizeThreshold() { return resizeThreshold; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return size; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return size == 0; } @@ -239,9 +212,9 @@ private void rehash(final int newCapacity) { /** * Primitive specialised forEach implementation. - *

    - * NB: Renamed from forEach to avoid overloading on parameter types of lambda - * expression, which doesn't play well with type inference in lambda expressions. + * + *

    NB: Renamed from forEach to avoid overloading on parameter types of lambda expression, which + * doesn't play well with type inference in lambda expressions. * * @param consumer a callback called for each key/value pair in the map. */ @@ -253,8 +226,8 @@ public void intForEach(final IntIntConsumer consumer) { for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { if (entries[keyIndex + 1] != missingValue) // lgtm [java/index-out-of-bounds] { - consumer.accept(entries[keyIndex], - entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] + consumer.accept( + entries[keyIndex], entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] } } } @@ -292,9 +265,7 @@ public boolean containsValue(final int value) { return found; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { if (size > 0) { Arrays.fill(entries, missingValue); @@ -303,8 +274,8 @@ public void clear() { } /** - * Compact the backing arrays by rehashing with a capacity just larger than current size - * and giving consideration to the load factor. + * Compact the backing arrays by rehashing with a capacity just larger than current size and + * giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); @@ -313,47 +284,34 @@ public void compact() { // ---------------- Boxed Versions Below ---------------- - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public Integer get(final Object key) { return valOrNull(get((int) key)); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public Integer put(final Integer key, final Integer value) { return valOrNull(put((int) key, (int) value)); } - - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean containsKey(final Object key) { return containsKey((int) key); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean containsValue(final Object value) { return containsValue((int) value); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void putAll(final Map map) { for (final Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public KeySet keySet() { if (null == keySet) { keySet = new KeySet(); @@ -362,9 +320,7 @@ public KeySet keySet() { return keySet; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public ValueCollection values() { if (null == values) { values = new ValueCollection(); @@ -373,9 +329,7 @@ public ValueCollection values() { return values; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public EntrySet entrySet() { if (null == entrySet) { entrySet = new EntrySet(); @@ -384,9 +338,7 @@ public EntrySet entrySet() { return entrySet; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public Integer remove(final Object key) { return valOrNull(remove((int) key)); } @@ -430,8 +382,8 @@ private void compactChain(int deleteKeyIndex) { final int hash = Hashing.evenHash(entries[keyIndex], mask); - if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) || - (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { + if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) + || (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { entries[deleteKeyIndex] = entries[keyIndex]; entries[deleteKeyIndex + 1] = entries[keyIndex + 1]; @@ -442,7 +394,8 @@ private void compactChain(int deleteKeyIndex) { } /** - * Get the minimum value stored in the map. If the map is empty then it will return {@link #missingValue()} + * Get the minimum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} * * @return the minimum value stored in the map. */ @@ -464,7 +417,8 @@ public int minValue() { } /** - * Get the maximum value stored in the map. If the map is empty then it will return {@link #missingValue()} + * Get the maximum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} * * @return the maximum value stored in the map. */ @@ -485,9 +439,7 @@ public int maxValue() { return max; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public String toString() { if (isEmpty()) { return "{}"; @@ -512,8 +464,8 @@ public String toString() { * * @param key key with which the specified value is associated * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or - * {@link #missingValue()} if there was no mapping for the key. + * @return the previous value associated with the specified key, or {@link #missingValue()} if + * there was no mapping for the key. */ public int replace(final int key, final int value) { int curValue = get(key); @@ -543,9 +495,7 @@ public boolean replace(final int key, final int oldValue, final int newValue) { return true; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @SuppressWarnings("unchecked") public boolean equals(final Object o) { if (this == o) { @@ -558,7 +508,6 @@ public boolean equals(final Object o) { final Map that = (Map) o; return size == that.size() && entrySet().equals(that.entrySet()); - } public int hashCode() { @@ -665,9 +614,7 @@ public void remove() { } } - /** - * Iterator over keys which supports access to unboxed keys. - */ + /** Iterator over keys which supports access to unboxed keys. */ public final class KeyIterator extends AbstractIterator implements Iterator { public Integer next() { return nextValue(); @@ -680,9 +627,7 @@ public int nextValue() { } } - /** - * Iterator over values which supports access to unboxed values. - */ + /** Iterator over values which supports access to unboxed values. */ public final class ValueIterator extends AbstractIterator implements Iterator { public Integer next() { return nextValue(); @@ -695,11 +640,8 @@ public int nextValue() { } } - /** - * Iterator over entries which supports access to unboxed keys and values. - */ - public final class EntryIterator - extends AbstractIterator + /** Iterator over entries which supports access to unboxed keys and values. */ + public final class EntryIterator extends AbstractIterator implements Iterator>, Entry { public Integer getKey() { return getIntKey(); @@ -774,8 +716,8 @@ public boolean equals(final Object o) { final Entry e = (Entry) o; - return (e.getKey() != null && e.getValue() != null) && - (e.getKey().equals(k) && e.getValue().equals(v)); + return (e.getKey() != null && e.getValue() != null) + && (e.getKey().equals(k) && e.getValue().equals(v)); } public String toString() { @@ -784,16 +726,12 @@ public String toString() { }; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int hashCode() { return getIntKey() ^ getIntValue(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean equals(final Object o) { if (this == o) { return true; @@ -809,15 +747,11 @@ public boolean equals(final Object o) { } } - /** - * Set of keys which supports optional cached iterators to avoid allocation. - */ + /** Set of keys which supports optional cached iterators to avoid allocation. */ public final class KeySet extends AbstractSet implements Serializable { private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public KeyIterator iterator() { KeyIterator keyIterator = this.keyIterator; if (null == keyIterator) { @@ -829,30 +763,22 @@ public KeyIterator iterator() { return keyIterator; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return Int2IntHashMap.this.size(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return Int2IntHashMap.this.isEmpty(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { Int2IntHashMap.this.clear(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object o) { return contains((int) o); } @@ -862,15 +788,11 @@ public boolean contains(final int key) { } } - /** - * Collection of values which supports optionally cached iterators to avoid allocation. - */ + /** Collection of values which supports optionally cached iterators to avoid allocation. */ public final class ValueCollection extends AbstractCollection { private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public ValueIterator iterator() { ValueIterator valueIterator = this.valueIterator; if (null == valueIterator) { @@ -882,16 +804,12 @@ public ValueIterator iterator() { return valueIterator; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return Int2IntHashMap.this.size(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object o) { return contains((int) o); } @@ -901,15 +819,11 @@ public boolean contains(final int key) { } } - /** - * Set of entries which supports optionally cached iterators to avoid allocation. - */ + /** Set of entries which supports optionally cached iterators to avoid allocation. */ public final class EntrySet extends AbstractSet> implements Serializable { private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public EntryIterator iterator() { EntryIterator entryIterator = this.entryIterator; if (null == entryIterator) { @@ -921,30 +835,22 @@ public EntryIterator iterator() { return entryIterator; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return Int2IntHashMap.this.size(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return Int2IntHashMap.this.isEmpty(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { Int2IntHashMap.this.clear(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object o) { final Entry entry = (Entry) o; final Integer value = get(entry.getKey()); diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java index ee9f48c7e..d0f28bf3c 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java @@ -16,23 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; +import static java.util.Objects.requireNonNull; + import java.io.Serializable; import java.util.AbstractCollection; import java.util.AbstractSet; @@ -42,19 +31,13 @@ import java.util.NoSuchElementException; import java.util.Objects; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; -import static java.util.Objects.requireNonNull; - - /** - * {@link Map} implementation specialised for int keys using open addressing and - * linear probing for cache efficient access. + * {@link Map} implementation specialised for int keys using open addressing and linear probing for + * cache efficient access. * * @param type of values stored in the {@link Map} */ -public class Int2ObjectHashMap - implements Map, Serializable { +public class Int2ObjectHashMap implements Map, Serializable { static final int MIN_CAPACITY = 8; private final float loadFactor; @@ -73,9 +56,7 @@ public Int2ObjectHashMap() { this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, true); } - public Int2ObjectHashMap( - final int initialCapacity, - final float loadFactor) { + public Int2ObjectHashMap(final int initialCapacity, final float loadFactor) { this(initialCapacity, loadFactor, true); } @@ -87,9 +68,7 @@ public Int2ObjectHashMap( * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. */ public Int2ObjectHashMap( - final int initialCapacity, - final float loadFactor, - final boolean shouldAvoidAllocation) { + final int initialCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { validateLoadFactor(loadFactor); this.loadFactor = loadFactor; @@ -138,8 +117,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. - * This is a function of the current capacity and load factor. + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -147,23 +126,17 @@ public int resizeThreshold() { return resizeThreshold; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return size; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return 0 == size; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean containsKey(final Object key) { return containsKey(((Integer) key).intValue()); } @@ -191,9 +164,7 @@ public boolean containsKey(final int key) { return found; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean containsValue(final Object value) { boolean found = false; final Object val = mapNullValue(value); @@ -209,9 +180,7 @@ public boolean containsValue(final Object value) { return found; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public V get(final Object key) { return get(((Integer) key).intValue()); } @@ -243,9 +212,7 @@ protected V getMapped(final int key) { return (V) value; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public V put(final Integer key, final V value) { return put(key.intValue(), value); } @@ -289,9 +256,7 @@ public V put(final int key, final V value) { return unmapNullValue(oldValue); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public V remove(final Object key) { return remove(((Integer) key).intValue()); } @@ -322,9 +287,7 @@ public V remove(final int key) { return unmapNullValue(value); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { if (size > 0) { Arrays.fill(values, null); @@ -333,26 +296,22 @@ public void clear() { } /** - * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current size - * and giving consideration to the load factor. + * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current + * size and giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void putAll(final Map map) { for (final Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public KeySet keySet() { if (null == keySet) { keySet = new KeySet(); @@ -361,9 +320,7 @@ public KeySet keySet() { return keySet; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public ValueCollection values() { if (null == valueCollection) { valueCollection = new ValueCollection(); @@ -372,9 +329,7 @@ public ValueCollection values() { return valueCollection; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public EntrySet entrySet() { if (null == entrySet) { entrySet = new EntrySet(); @@ -383,9 +338,7 @@ public EntrySet entrySet() { return entrySet; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public String toString() { if (isEmpty()) { return "{}"; @@ -397,7 +350,8 @@ public String toString() { final StringBuilder sb = new StringBuilder().append('{'); while (true) { entryIterator.next(); - sb.append(entryIterator.getIntKey()).append('=') + sb.append(entryIterator.getIntKey()) + .append('=') .append(unmapNullValue(entryIterator.getValue())); if (!entryIterator.hasNext()) { return sb.append('}').toString(); @@ -406,9 +360,7 @@ public String toString() { } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean equals(final Object o) { if (this == o) { return true; @@ -437,9 +389,7 @@ public boolean equals(final Object o) { return true; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int hashCode() { int result = 0; @@ -467,8 +417,8 @@ protected V unmapNullValue(final Object value) { * * @param key key with which the specified value is associated * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or - * {@code null} if there was no mapping for the key. + * @return the previous value associated with the specified key, or {@code null} if there was no + * mapping for the key. */ public V replace(final int key, final V value) { V curValue = get(key); @@ -545,8 +495,8 @@ private void compactChain(int deleteIndex) { final int hash = Hashing.hash(keys[index], mask); - if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || - (hash <= deleteIndex && deleteIndex <= index)) { + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) + || (hash <= deleteIndex && deleteIndex <= index)) { keys[deleteIndex] = keys[index]; values[deleteIndex] = values[index]; @@ -560,15 +510,11 @@ private void compactChain(int deleteIndex) { // Sets and Collections /////////////////////////////////////////////////////////////////////////////////////////////// - /** - * Set of keys which supports optionally cached iterators to avoid allocation. - */ + /** Set of keys which supports optionally cached iterators to avoid allocation. */ public final class KeySet extends AbstractSet implements Serializable { private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public KeyIterator iterator() { KeyIterator keyIterator = this.keyIterator; if (null == keyIterator) { @@ -604,15 +550,11 @@ public void clear() { } } - /** - * Collection of values which supports optionally cached iterators to avoid allocation. - */ + /** Collection of values which supports optionally cached iterators to avoid allocation. */ public final class ValueCollection extends AbstractCollection implements Serializable { private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public ValueIterator iterator() { ValueIterator valueIterator = this.valueIterator; if (null == valueIterator) { @@ -636,15 +578,11 @@ public void clear() { } } - /** - * Set of entries which supports access via an optionally cached iterator to avoid allocation. - */ + /** Set of entries which supports access via an optionally cached iterator to avoid allocation. */ public final class EntrySet extends AbstractSet> implements Serializable { private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public EntryIterator iterator() { EntryIterator entryIterator = this.entryIterator; if (null == entryIterator) { @@ -663,9 +601,7 @@ public void clear() { Int2ObjectHashMap.this.clear(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object o) { final Entry entry = (Entry) o; final int key = (Integer) entry.getKey(); @@ -754,9 +690,7 @@ final void reset() { } } - /** - * Iterator over values. - */ + /** Iterator over values. */ public class ValueIterator extends AbstractIterator { public V next() { findNext(); @@ -765,9 +699,7 @@ public V next() { } } - /** - * Iterator over keys which supports access to unboxed keys. - */ + /** Iterator over keys which supports access to unboxed keys. */ public class KeyIterator extends AbstractIterator { public Integer next() { return nextInt(); @@ -780,11 +712,8 @@ public int nextInt() { } } - /** - * Iterator over entries which supports access to unboxed keys and values. - */ - public class EntryIterator - extends AbstractIterator> + /** Iterator over entries which supports access to unboxed keys and values. */ + public class EntryIterator extends AbstractIterator> implements Entry { public Entry next() { findNext(); @@ -823,8 +752,8 @@ public boolean equals(final Object o) { final Entry e = (Entry) o; - return (e.getKey() != null && e.getKey().equals(k)) && - ((e.getValue() == null && v == null) || e.getValue().equals(v)); + return (e.getKey() != null && e.getKey().equals(k)) + && ((e.getValue() == null && v == null) || e.getValue().equals(v)); } public String toString() { diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java index 0fb9e7fd0..748ba3b2b 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java @@ -16,29 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; -/** - * This is an (int, int) primitive specialisation of a BiConsumer - */ +/** This is an (int, int) primitive specialisation of a BiConsumer */ @FunctionalInterface -public interface -IntIntConsumer { +public interface IntIntConsumer { /** * Accept two values that comes as a tuple of ints. * diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java index d1d7d7ce9..091776144 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java @@ -16,23 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2020 Real Logic Limited. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; + import java.io.Serializable; import java.util.AbstractCollection; import java.util.AbstractSet; @@ -42,12 +30,7 @@ import java.util.NoSuchElementException; import java.util.Objects; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; - -/** - * A open addressing with linear probing hash map specialised for primitive key and value pairs. - */ +/** A open addressing with linear probing hash map specialised for primitive key and value pairs. */ public class Long2LongHashMap implements Map, Serializable { static final int MIN_CAPACITY = 8; @@ -67,9 +50,7 @@ public Long2LongHashMap(final long missingValue) { } public Long2LongHashMap( - final int initialCapacity, - final float loadFactor, - final long missingValue) { + final int initialCapacity, final float loadFactor, final long missingValue) { this(initialCapacity, loadFactor, missingValue, true); } @@ -121,8 +102,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. - * This is a function of the current capacity and load factor. + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -130,16 +111,12 @@ public int resizeThreshold() { return resizeThreshold; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return size; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return size == 0; } @@ -239,9 +216,9 @@ private void rehash(final int newCapacity) { /** * Primitive specialised forEach implementation. - *

    - * NB: Renamed from forEach to avoid overloading on parameter types of lambda - * expression, which doesn't play well with type inference in lambda expressions. + * + *

    NB: Renamed from forEach to avoid overloading on parameter types of lambda expression, which + * doesn't play well with type inference in lambda expressions. * * @param consumer a callback called for each key/value pair in the map. */ @@ -253,8 +230,8 @@ public void longForEach(final LongLongConsumer consumer) { for (int keyIndex = 0; keyIndex < length; keyIndex += 2) { if (entries[keyIndex + 1] != missingValue) // lgtm [java/index-out-of-bounds] { - consumer.accept(entries[keyIndex], - entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] + consumer.accept( + entries[keyIndex], entries[keyIndex + 1]); // lgtm [java/index-out-of-bounds] } } } @@ -292,9 +269,7 @@ public boolean containsValue(final long value) { return found; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { if (size > 0) { Arrays.fill(entries, missingValue); @@ -303,8 +278,8 @@ public void clear() { } /** - * Compact the backing arrays by rehashing with a capacity just larger than current size - * and giving consideration to the load factor. + * Compact the backing arrays by rehashing with a capacity just larger than current size and + * giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); @@ -313,46 +288,34 @@ public void compact() { // ---------------- Boxed Versions Below ---------------- - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public Long get(final Object key) { return valOrNull(get((long) key)); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public Long put(final Long key, final Long value) { return valOrNull(put((long) key, (long) value)); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean containsKey(final Object key) { return containsKey((long) key); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean containsValue(final Object value) { return containsValue((long) value); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void putAll(final Map map) { for (final Map.Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public KeySet keySet() { if (null == keySet) { keySet = new KeySet(); @@ -361,9 +324,7 @@ public KeySet keySet() { return keySet; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public ValueCollection values() { if (null == values) { values = new ValueCollection(); @@ -372,9 +333,7 @@ public ValueCollection values() { return values; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public EntrySet entrySet() { if (null == entrySet) { entrySet = new EntrySet(); @@ -383,9 +342,7 @@ public EntrySet entrySet() { return entrySet; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public Long remove(final Object key) { return valOrNull(remove((long) key)); } @@ -429,8 +386,8 @@ private void compactChain(int deleteKeyIndex) { final int hash = Hashing.evenHash(entries[keyIndex], mask); - if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) || - (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { + if ((keyIndex < hash && (hash <= deleteKeyIndex || deleteKeyIndex <= keyIndex)) + || (hash <= deleteKeyIndex && deleteKeyIndex <= keyIndex)) { entries[deleteKeyIndex] = entries[keyIndex]; entries[deleteKeyIndex + 1] = entries[keyIndex + 1]; @@ -441,7 +398,8 @@ private void compactChain(int deleteKeyIndex) { } /** - * Get the minimum value stored in the map. If the map is empty then it will return {@link #missingValue()} + * Get the minimum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} * * @return the minimum value stored in the map. */ @@ -463,7 +421,8 @@ public long minValue() { } /** - * Get the maximum value stored in the map. If the map is empty then it will return {@link #missingValue()} + * Get the maximum value stored in the map. If the map is empty then it will return {@link + * #missingValue()} * * @return the maximum value stored in the map. */ @@ -484,9 +443,7 @@ public long maxValue() { return max; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public String toString() { if (isEmpty()) { return "{}"; @@ -511,8 +468,8 @@ public String toString() { * * @param key key with which the specified value is associated * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or - * {@link #missingValue()} if there was no mapping for the key. + * @return the previous value associated with the specified key, or {@link #missingValue()} if + * there was no mapping for the key. */ public long replace(final long key, final long value) { long currentValue = get(key); @@ -542,9 +499,7 @@ public boolean replace(final long key, final long oldValue, final long newValue) return true; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean equals(final Object o) { if (this == o) { return true; @@ -556,7 +511,6 @@ public boolean equals(final Object o) { final Map that = (Map) o; return size == that.size() && entrySet().equals(that.entrySet()); - } public int hashCode() { @@ -662,9 +616,7 @@ public void remove() { } } - /** - * Iterator over keys which supports access to unboxed keys. - */ + /** Iterator over keys which supports access to unboxed keys. */ public final class KeyIterator extends AbstractIterator implements Iterator { public Long next() { return nextValue(); @@ -676,9 +628,7 @@ public long nextValue() { } } - /** - * Iterator over values which supports access to unboxed values. - */ + /** Iterator over values which supports access to unboxed values. */ public final class ValueIterator extends AbstractIterator implements Iterator { public Long next() { return nextValue(); @@ -690,11 +640,8 @@ public long nextValue() { } } - /** - * Iterator over entries which supports access to unboxed keys and values. - */ - public final class EntryIterator - extends AbstractIterator + /** Iterator over entries which supports access to unboxed keys and values. */ + public final class EntryIterator extends AbstractIterator implements Iterator>, Entry { public Long getKey() { return getLongKey(); @@ -769,8 +716,8 @@ public boolean equals(final Object o) { final Map.Entry e = (Entry) o; - return (e.getKey() != null && e.getValue() != null) && - (e.getKey().equals(k) && e.getValue().equals(v)); + return (e.getKey() != null && e.getValue() != null) + && (e.getKey().equals(k) && e.getValue().equals(v)); } public String toString() { @@ -779,16 +726,12 @@ public String toString() { }; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int hashCode() { return Hashing.hashCode(getLongKey()) ^ Hashing.hashCode(getLongValue()); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean equals(final Object o) { if (this == o) { return true; @@ -804,15 +747,11 @@ public boolean equals(final Object o) { } } - /** - * Set of keys which supports optional cached iterators to avoid allocation. - */ + /** Set of keys which supports optional cached iterators to avoid allocation. */ public final class KeySet extends AbstractSet implements Serializable { private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public KeyIterator iterator() { KeyIterator keyIterator = this.keyIterator; if (null == keyIterator) { @@ -824,30 +763,22 @@ public KeyIterator iterator() { return keyIterator; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return Long2LongHashMap.this.size(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return Long2LongHashMap.this.isEmpty(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { Long2LongHashMap.this.clear(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object o) { return contains((long) o); } @@ -857,15 +788,11 @@ public boolean contains(final long key) { } } - /** - * Collection of values which supports optionally cached iterators to avoid allocation. - */ + /** Collection of values which supports optionally cached iterators to avoid allocation. */ public final class ValueCollection extends AbstractCollection { private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public ValueIterator iterator() { ValueIterator valueIterator = this.valueIterator; if (null == valueIterator) { @@ -877,16 +804,12 @@ public ValueIterator iterator() { return valueIterator; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return Long2LongHashMap.this.size(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object o) { return contains((long) o); } @@ -896,15 +819,11 @@ public boolean contains(final long key) { } } - /** - * Set of entries which supports optionally cached iterators to avoid allocation. - */ + /** Set of entries which supports optionally cached iterators to avoid allocation. */ public final class EntrySet extends AbstractSet> implements Serializable { private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public EntryIterator iterator() { EntryIterator entryIterator = this.entryIterator; if (null == entryIterator) { @@ -916,30 +835,22 @@ public EntryIterator iterator() { return entryIterator; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return Long2LongHashMap.this.size(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return Long2LongHashMap.this.isEmpty(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { Long2LongHashMap.this.clear(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object o) { if (!(o instanceof Entry)) { return false; diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java index 01ce2b507..b5cf3e722 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java @@ -16,23 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; +import static java.util.Objects.requireNonNull; + import java.io.Serializable; import java.util.AbstractCollection; import java.util.AbstractSet; @@ -42,19 +31,13 @@ import java.util.NoSuchElementException; import java.util.Objects; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; -import static java.util.Objects.requireNonNull; - - /** - * {@link Map} implementation specialised for long keys using open addressing and - * linear probing for cache efficient access. + * {@link Map} implementation specialised for long keys using open addressing and linear probing for + * cache efficient access. * * @param type of values stored in the {@link Map} */ -public class Long2ObjectHashMap - implements Map, Serializable { +public class Long2ObjectHashMap implements Map, Serializable { static final int MIN_CAPACITY = 8; private final float loadFactor; @@ -73,9 +56,7 @@ public Long2ObjectHashMap() { this(MIN_CAPACITY, Hashing.DEFAULT_LOAD_FACTOR, true); } - public Long2ObjectHashMap( - final int initialCapacity, - final float loadFactor) { + public Long2ObjectHashMap(final int initialCapacity, final float loadFactor) { this(initialCapacity, loadFactor, true); } @@ -87,9 +68,7 @@ public Long2ObjectHashMap( * @param shouldAvoidAllocation should allocation be avoided by caching iterators and map entries. */ public Long2ObjectHashMap( - final int initialCapacity, - final float loadFactor, - final boolean shouldAvoidAllocation) { + final int initialCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { validateLoadFactor(loadFactor); this.loadFactor = loadFactor; @@ -138,8 +117,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. - * This is a function of the current capacity and load factor. + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -147,23 +126,17 @@ public int resizeThreshold() { return resizeThreshold; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return size; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return 0 == size; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean containsKey(final Object key) { return containsKey(((Long) key).longValue()); } @@ -191,9 +164,7 @@ public boolean containsKey(final long key) { return found; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean containsValue(final Object value) { boolean found = false; final Object val = mapNullValue(value); @@ -209,9 +180,7 @@ public boolean containsValue(final Object value) { return found; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public V get(final Object key) { return get(((Long) key).longValue()); } @@ -243,9 +212,7 @@ protected V getMapped(final long key) { return (V) value; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public V put(final Long key, final V value) { return put(key.longValue(), value); } @@ -289,9 +256,7 @@ public V put(final long key, final V value) { return unmapNullValue(oldValue); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public V remove(final Object key) { return remove(((Long) key).longValue()); } @@ -322,9 +287,7 @@ public V remove(final long key) { return unmapNullValue(value); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { if (size > 0) { Arrays.fill(values, null); @@ -333,26 +296,22 @@ public void clear() { } /** - * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current size - * and giving consideration to the load factor. + * Compact the {@link Map} backing arrays by rehashing with a capacity just larger than current + * size and giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0d / loadFactor)); rehash(findNextPositivePowerOfTwo(Math.max(MIN_CAPACITY, idealCapacity))); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void putAll(final Map map) { for (final Entry entry : map.entrySet()) { put(entry.getKey(), entry.getValue()); } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public KeySet keySet() { if (null == keySet) { keySet = new KeySet(); @@ -361,9 +320,7 @@ public KeySet keySet() { return keySet; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public ValueCollection values() { if (null == valueCollection) { valueCollection = new ValueCollection(); @@ -372,9 +329,7 @@ public ValueCollection values() { return valueCollection; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public EntrySet entrySet() { if (null == entrySet) { entrySet = new EntrySet(); @@ -383,9 +338,7 @@ public EntrySet entrySet() { return entrySet; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public String toString() { if (isEmpty()) { return "{}"; @@ -397,7 +350,8 @@ public String toString() { final StringBuilder sb = new StringBuilder().append('{'); while (true) { entryIterator.next(); - sb.append(entryIterator.getLongKey()).append('=') + sb.append(entryIterator.getLongKey()) + .append('=') .append(unmapNullValue(entryIterator.getValue())); if (!entryIterator.hasNext()) { return sb.append('}').toString(); @@ -406,9 +360,7 @@ public String toString() { } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean equals(final Object o) { if (this == o) { return true; @@ -437,9 +389,7 @@ public boolean equals(final Object o) { return true; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int hashCode() { int result = 0; @@ -467,8 +417,8 @@ protected V unmapNullValue(final Object value) { * * @param key key with which the specified value is associated * @param value value to be associated with the specified key - * @return the previous value associated with the specified key, or - * {@code null} if there was no mapping for the key. + * @return the previous value associated with the specified key, or {@code null} if there was no + * mapping for the key. */ public V replace(final long key, final V value) { V curValue = get(key); @@ -545,8 +495,8 @@ private void compactChain(int deleteIndex) { final int hash = Hashing.hash(keys[index], mask); - if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || - (hash <= deleteIndex && deleteIndex <= index)) { + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) + || (hash <= deleteIndex && deleteIndex <= index)) { keys[deleteIndex] = keys[index]; values[deleteIndex] = values[index]; @@ -560,15 +510,11 @@ private void compactChain(int deleteIndex) { // Sets and Collections /////////////////////////////////////////////////////////////////////////////////////////////// - /** - * Set of keys which supports optionally cached iterators to avoid allocation. - */ + /** Set of keys which supports optionally cached iterators to avoid allocation. */ public final class KeySet extends AbstractSet implements Serializable { private final KeyIterator keyIterator = shouldAvoidAllocation ? new KeyIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public KeyIterator iterator() { KeyIterator keyIterator = this.keyIterator; if (null == keyIterator) { @@ -604,15 +550,11 @@ public void clear() { } } - /** - * Collection of values which supports optionally cached iterators to avoid allocation. - */ + /** Collection of values which supports optionally cached iterators to avoid allocation. */ public final class ValueCollection extends AbstractCollection implements Serializable { private final ValueIterator valueIterator = shouldAvoidAllocation ? new ValueIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public ValueIterator iterator() { ValueIterator valueIterator = this.valueIterator; if (null == valueIterator) { @@ -636,15 +578,11 @@ public void clear() { } } - /** - * Set of entries which supports access via an optionally cached iterator to avoid allocation. - */ + /** Set of entries which supports access via an optionally cached iterator to avoid allocation. */ public final class EntrySet extends AbstractSet> implements Serializable { private final EntryIterator entryIterator = shouldAvoidAllocation ? new EntryIterator() : null; - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public EntryIterator iterator() { EntryIterator entryIterator = this.entryIterator; if (null == entryIterator) { @@ -663,9 +601,7 @@ public void clear() { Long2ObjectHashMap.this.clear(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object o) { final Entry entry = (Entry) o; final long key = (Long) entry.getKey(); @@ -754,9 +690,7 @@ final void reset() { } } - /** - * Iterator over values. - */ + /** Iterator over values. */ public class ValueIterator extends AbstractIterator { public V next() { findNext(); @@ -765,9 +699,7 @@ public V next() { } } - /** - * Iterator over keys which supports access to unboxed keys. - */ + /** Iterator over keys which supports access to unboxed keys. */ public class KeyIterator extends AbstractIterator { public Long next() { return nextLong(); @@ -780,12 +712,8 @@ public long nextLong() { } } - /** - * Iterator over entries which supports access to unboxed keys and values. - */ - public class EntryIterator - extends AbstractIterator> - implements Entry { + /** Iterator over entries which supports access to unboxed keys and values. */ + public class EntryIterator extends AbstractIterator> implements Entry { public Entry next() { findNext(); if (shouldAvoidAllocation) { @@ -823,8 +751,8 @@ public boolean equals(final Object o) { final Entry e = (Entry) o; - return (e.getKey() != null && e.getKey().equals(k)) && - ((e.getValue() == null && v == null) || e.getValue().equals(v)); + return (e.getKey() != null && e.getKey().equals(k)) + && ((e.getValue() == null && v == null) || e.getValue().equals(v)); } public String toString() { diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java index 8a7ae4733..f8c5154de 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java @@ -16,23 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2020 Real Logic Limited. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; import java.io.Serializable; import java.lang.reflect.Array; @@ -43,28 +30,25 @@ import java.util.NoSuchElementException; import java.util.Set; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; - /** - * Open-addressing with linear-probing expandable hash set. Allocation free in steady state use when expanded. - *

    - * By storing elements as long primitives this significantly reduces memory consumption compared with Java's builtin - * HashSet<Long>. It implements Set<Long> for convenience, but calling - * functionality via those methods can add boxing overhead to your usage. - *

    - * This class is not Threadsafe. - *

    - * This HashSet caches its iterator object by default, so nested iteration is not supported. You can override this - * behaviour at construction by indicating that the iterator should not be cached. + * Open-addressing with linear-probing expandable hash set. Allocation free in steady state use when + * expanded. + * + *

    By storing elements as long primitives this significantly reduces memory consumption compared + * with Java's builtin HashSet<Long>. It implements Set<Long> + * for convenience, but calling functionality via those methods can add boxing overhead to your + * usage. + * + *

    This class is not Threadsafe. + * + *

    This HashSet caches its iterator object by default, so nested iteration is not supported. You + * can override this behaviour at construction by indicating that the iterator should not be cached. * * @see LongIterator * @see Set */ public class LongHashSet extends AbstractSet implements Serializable { - /** - * The initial capacity used when none is specified in the constructor. - */ + /** The initial capacity used when none is specified in the constructor. */ public static final int DEFAULT_INITIAL_CAPACITY = 8; static final long MISSING_VALUE = -1; @@ -80,54 +64,51 @@ public class LongHashSet extends AbstractSet implements Serializable { private LongIterator iterator; /** - * Construct a hash set with {@link #DEFAULT_INITIAL_CAPACITY}, {@link Hashing#DEFAULT_LOAD_FACTOR}, - * and iterator caching support. + * Construct a hash set with {@link #DEFAULT_INITIAL_CAPACITY}, {@link + * Hashing#DEFAULT_LOAD_FACTOR}, and iterator caching support. */ public LongHashSet() { this(DEFAULT_INITIAL_CAPACITY); } /** - * Construct a hash set with a proposed capacity, {@link Hashing#DEFAULT_LOAD_FACTOR}, - * and iterator caching support. + * Construct a hash set with a proposed capacity, {@link Hashing#DEFAULT_LOAD_FACTOR}, and + * iterator caching support. * * @param proposedCapacity for the initial capacity of the set. */ - public LongHashSet( - final int proposedCapacity) { + public LongHashSet(final int proposedCapacity) { this(proposedCapacity, Hashing.DEFAULT_LOAD_FACTOR, true); } /** - * Construct a hash set with a proposed initial capacity, load factor, and iterator caching support. + * Construct a hash set with a proposed initial capacity, load factor, and iterator caching + * support. * * @param proposedCapacity for the initial capacity of the set. * @param loadFactor to be used for resizing. */ - public LongHashSet( - final int proposedCapacity, - final float loadFactor) { + public LongHashSet(final int proposedCapacity, final float loadFactor) { this(proposedCapacity, loadFactor, true); } /** - * Construct a hash set with a proposed initial capacity, load factor, and indicated iterator caching support. + * Construct a hash set with a proposed initial capacity, load factor, and indicated iterator + * caching support. * * @param proposedCapacity for the initial capacity of the set. * @param loadFactor to be used for resizing. * @param shouldAvoidAllocation should the iterator be cached to avoid further allocation. */ public LongHashSet( - final int proposedCapacity, - final float loadFactor, - final boolean shouldAvoidAllocation) { + final int proposedCapacity, final float loadFactor, final boolean shouldAvoidAllocation) { validateLoadFactor(loadFactor); this.shouldAvoidAllocation = shouldAvoidAllocation; this.loadFactor = loadFactor; sizeOfArrayValues = 0; - final int capacity = findNextPositivePowerOfTwo( - Math.max(DEFAULT_INITIAL_CAPACITY, proposedCapacity)); + final int capacity = + findNextPositivePowerOfTwo(Math.max(DEFAULT_INITIAL_CAPACITY, proposedCapacity)); resizeThreshold = (int) (capacity * loadFactor); // @DoNotSub values = new long[capacity]; Arrays.fill(values, MISSING_VALUE); @@ -152,8 +133,8 @@ public int capacity() { } /** - * Get the actual threshold which when reached the map will resize. - * This is a function of the current capacity and load factor. + * Get the actual threshold which when reached the map will resize. This is a function of the + * current capacity and load factor. * * @return the threshold when the map will resize. */ @@ -161,9 +142,7 @@ public int resizeThreshold() { return resizeThreshold; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean add(final Long value) { return add(value.longValue()); } @@ -235,9 +214,7 @@ private void rehash(final int newCapacity) { values = tempValues; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean remove(final Object value) { return value instanceof Long && remove(((Long) value).longValue()); } @@ -291,8 +268,8 @@ void compactChain(int deleteIndex) { final int hash = Hashing.hash(values[index], mask); - if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) || - (hash <= deleteIndex && deleteIndex <= index)) { + if ((index < hash && (hash <= deleteIndex || deleteIndex <= index)) + || (hash <= deleteIndex && deleteIndex <= index)) { values[deleteIndex] = values[index]; values[index] = MISSING_VALUE; @@ -302,17 +279,15 @@ void compactChain(int deleteIndex) { } /** - * Compact the backing arrays by rehashing with a capacity just larger than current size - * and giving consideration to the load factor. + * Compact the backing arrays by rehashing with a capacity just larger than current size and + * giving consideration to the load factor. */ public void compact() { final int idealCapacity = (int) Math.round(size() * (1.0 / loadFactor)); rehash(findNextPositivePowerOfTwo(Math.max(DEFAULT_INITIAL_CAPACITY, idealCapacity))); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean contains(final Object value) { return value instanceof Long && contains(((Long) value).longValue()); } @@ -344,23 +319,17 @@ public boolean contains(final long value) { return false; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int size() { return sizeOfArrayValues + (containsMissingValue ? 1 : 0); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean isEmpty() { return size() == 0; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public void clear() { if (size() > 0) { Arrays.fill(values, MISSING_VALUE); @@ -369,9 +338,7 @@ public void clear() { } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean addAll(final Collection coll) { boolean added = false; @@ -423,8 +390,8 @@ public boolean containsAll(final LongHashSet other) { /** * Fast Path set difference for comparison with another LongHashSet. - *

    - * Note: garbage free in the identical case, allocates otherwise. + * + *

    Note: garbage free in the identical case, allocates otherwise. * * @param other the other set to subtract * @return null if identical, otherwise the set of differences @@ -453,9 +420,7 @@ public LongHashSet difference(final LongHashSet other) { return difference; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean removeAll(final Collection coll) { boolean removed = false; @@ -467,8 +432,8 @@ public boolean removeAll(final Collection coll) { } /** - * Alias for {@link #removeAll(Collection)} for the specialized case when removing another LongHashSet, - * avoids boxing and allocations + * Alias for {@link #removeAll(Collection)} for the specialized case when removing another + * LongHashSet, avoids boxing and allocations * * @param coll containing the values to be removed. * @return {@code true} if this set changed as a result of the call @@ -489,9 +454,7 @@ public boolean removeAll(final LongHashSet coll) { return acc; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public LongIterator iterator() { LongIterator iterator = this.iterator; if (null == iterator) { @@ -514,9 +477,7 @@ public void copy(final LongHashSet that) { this.containsMissingValue = that.containsMissingValue; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public String toString() { final StringBuilder sb = new StringBuilder(); sb.append('{'); @@ -540,9 +501,7 @@ public String toString() { return sb.toString(); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @SuppressWarnings("unchecked") public T[] toArray(final T[] a) { final Class componentType = a.getClass().getComponentType(); @@ -557,9 +516,7 @@ public T[] toArray(final T[] a) { return arrayCopy; } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public Object[] toArray() { final Object[] arrayCopy = new Object[size()]; copyValues(arrayCopy); @@ -581,9 +538,7 @@ private void copyValues(final Object[] arrayCopy) { } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public boolean equals(final Object other) { if (other == this) { return true; @@ -592,9 +547,9 @@ public boolean equals(final Object other) { if (other instanceof LongHashSet) { final LongHashSet otherSet = (LongHashSet) other; - return otherSet.containsMissingValue == containsMissingValue && - otherSet.sizeOfArrayValues == sizeOfArrayValues && - containsAll(otherSet); + return otherSet.containsMissingValue == containsMissingValue + && otherSet.sizeOfArrayValues == sizeOfArrayValues + && containsAll(otherSet); } if (!(other instanceof Set)) { @@ -613,9 +568,7 @@ public boolean equals(final Object other) { } } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ public int hashCode() { int hashCode = 0; for (final long value : values) { @@ -631,9 +584,7 @@ public int hashCode() { return hashCode; } - /** - * Iterator which supports unboxed access to values. - */ + /** Iterator which supports unboxed access to values. */ public final class LongIterator implements Iterator, Serializable { private int remaining; private int positionCounter; @@ -731,10 +682,8 @@ private void findNext() { throw new NoSuchElementException(); } - private int position( - final long[] values) { + private int position(final long[] values) { return positionCounter & (values.length - 1); } } } - diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java index cdf334e9b..5ac25a9a1 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java @@ -1,6 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.collections; - import java.util.Arrays; public class LongList { @@ -120,4 +137,3 @@ public boolean isEmpty() { return size == 0; } } - diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java index f3097236b..c30bdd85c 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java @@ -16,29 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -/* - * Copyright 2014-2020 Real Logic Limited. - * - * 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 - * - * https://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 co.elastic.apm.otel.profiler.collections; -/** - * This is an (long, long) primitive specialisation of a BiConsumer - */ +/** This is an (long, long) primitive specialisation of a BiConsumer */ @FunctionalInterface -public interface -LongLongConsumer { +public interface LongLongConsumer { /** * Accept two values that comes as a tuple of longs. * diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java index c90e9dd84..b2ad902a5 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java @@ -1,39 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.config; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; /** - * This matcher is used in for example to disable tracing for certain URLs. - * The advantage of this class compared to alternatives is that {@linkplain #matches(CharSequence) matching} strings is completely allocation free. - *

    - * The wildcard matcher supports the {@code *} wildcard which matches zero or more characters. - * By default, matches are a case insensitive. - * Single character wildcards like {@code f?o} are not supported. - *

    - *

    - * The syntax should be very familiar to any developer. - * The most common use cases are ignoring URLs paths starting with a specific string like {@code /resources/*} or /heartbeat/* - * and ignoring URLs by file ending like {@code *.js}. - * It also allows to have a single configuration option which, - * depending on the input string, - * allows for prefix, postfix and infix matching. - * This implementation is also very fast, - * as it just resorts to {@link String#startsWith(String)}, - * {@link String#endsWith(String)} and {@link String#contains(CharSequence)}. - *

    + * This matcher is used in for example to disable tracing for certain URLs. The advantage of this + * class compared to alternatives is that {@linkplain #matches(CharSequence) matching} strings is + * completely allocation free. + * + *

    The wildcard matcher supports the {@code *} wildcard which matches zero or more characters. By + * default, matches are a case insensitive. Single character wildcards like {@code f?o} are not + * supported. + * + *

    The syntax should be very familiar to any developer. The most common use cases are ignoring + * URLs paths starting with a specific string like {@code /resources/*} or /heartbeat/* + * and ignoring URLs by file ending like {@code *.js}. It also allows to have a single configuration + * option which, depending on the input string, allows for prefix, postfix and infix matching. This + * implementation is also very fast, as it just resorts to {@link String#startsWith(String)}, {@link + * String#endsWith(String)} and {@link String#contains(CharSequence)}. */ // don't use for-each as it allocates memory by instantiating an iterator @SuppressWarnings("ForLoopReplaceableByForEach") public abstract class WildcardMatcher { public static final String DOCUMENTATION = - "This option supports the wildcard `*`, which matches zero or more characters.\n" + - "Examples: `/foo/*/bar/*/baz*`, `*foo*`.\n" + - "Matching is case insensitive by default.\n" + - "Prepending an element with `(?-i)` makes the matching case sensitive."; + "This option supports the wildcard `*`, which matches zero or more characters.\n" + + "Examples: `/foo/*/bar/*/baz*`, `*foo*`.\n" + + "Matching is case insensitive by default.\n" + + "Prepending an element with `(?-i)` makes the matching case sensitive."; private static final String CASE_INSENSITIVE_PREFIX = "(?i)"; private static final String CASE_SENSITIVE_PREFIX = "(?-i)"; private static final String WILDCARD = "*"; @@ -54,17 +67,14 @@ public static List matchAllList() { /** * Constructs a new {@link WildcardMatcher} via a wildcard string. - *

    - * It supports the {@code *} wildcard which matches zero or more characters. - *

    - *

    - * By default, matches are a case insensitive. - * Prepend {@code (?-i)} to your pattern to make it case sensitive. - * Example: {@code (?-i)foo*} matches the string {@code foobar} but does not match {@code FOOBAR}. - *

    - *

    - * It does NOT support single character wildcards like {@code f?o} - *

    + * + *

    It supports the {@code *} wildcard which matches zero or more characters. + * + *

    By default, matches are a case insensitive. Prepend {@code (?-i)} to your pattern to make it + * case sensitive. Example: {@code (?-i)foo*} matches the string {@code foobar} but does not match + * {@code FOOBAR}. + * + *

    It does NOT support single character wildcards like {@code f?o} * * @param wildcardString The wildcard string. * @return The {@link WildcardMatcher} @@ -81,18 +91,20 @@ public static WildcardMatcher valueOf(final String wildcardString) { String[] split = matcher.split("\\*"); if (split.length == 1) { - return new SimpleWildcardMatcher(split[0], matcher.startsWith(WILDCARD), - matcher.endsWith(WILDCARD), ignoreCase); + return new SimpleWildcardMatcher( + split[0], matcher.startsWith(WILDCARD), matcher.endsWith(WILDCARD), ignoreCase); } List matchers = new ArrayList<>(split.length); for (int i = 0; i < split.length; i++) { boolean isFirst = i == 0; boolean isLast = i == split.length - 1; - matchers.add(new SimpleWildcardMatcher(split[i], - !isFirst || matcher.startsWith(WILDCARD), - !isLast || matcher.endsWith(WILDCARD), - ignoreCase)); + matchers.add( + new SimpleWildcardMatcher( + split[i], + !isFirst || matcher.startsWith(WILDCARD), + !isLast || matcher.endsWith(WILDCARD), + ignoreCase)); } return new CompoundWildcardMatcher(wildcardString, matcher, matchers); } @@ -120,7 +132,8 @@ public static boolean isNoneMatch(List matchers, @Nullable Char } /** - * Returns the first {@link WildcardMatcher} {@linkplain WildcardMatcher#matches(CharSequence) matching} the provided string. + * Returns the first {@link WildcardMatcher} {@linkplain WildcardMatcher#matches(CharSequence) + * matching} the provided string. * * @param matchers the matchers which should be used to match the provided string * @param s the string to match against @@ -135,7 +148,8 @@ public static WildcardMatcher anyMatch(List matchers, @Nullable } /** - * Returns the first {@link WildcardMatcher} {@linkplain WildcardMatcher#matches(CharSequence) matching} the provided partitioned string. + * Returns the first {@link WildcardMatcher} {@linkplain WildcardMatcher#matches(CharSequence) + * matching} the provided partitioned string. * * @param matchers the matchers which should be used to match the provided string * @param firstPart The first part of the string to match against. @@ -144,8 +158,8 @@ public static WildcardMatcher anyMatch(List matchers, @Nullable * @see #matches(CharSequence, CharSequence) */ @Nullable - public static WildcardMatcher anyMatch(List matchers, CharSequence firstPart, - @Nullable CharSequence secondPart) { + public static WildcardMatcher anyMatch( + List matchers, CharSequence firstPart, @Nullable CharSequence secondPart) { for (int i = 0; i < matchers.size(); i++) { if (matchers.get(i).matches(firstPart, secondPart)) { return matchers.get(i); @@ -158,8 +172,13 @@ public static WildcardMatcher anyMatch(List matchers, CharSeque * Based on https://stackoverflow.com/a/29809553/1125055 * Thx to Zach Vorhies */ - public static int indexOfIgnoreCase(final CharSequence haystack1, final CharSequence haystack2, - final String needle, final boolean ignoreCase, final int start, final int end) { + public static int indexOfIgnoreCase( + final CharSequence haystack1, + final CharSequence haystack2, + final String needle, + final boolean ignoreCase, + final int start, + final int end) { if (start < 0) { return -1; } @@ -182,7 +201,8 @@ public static int indexOfIgnoreCase(final CharSequence haystack1, final CharSequ int ii = i; while (ii < totalHaystackLength && j < needleLength) { char c = - ignoreCase ? Character.toLowerCase(charAt(ii, haystack1, haystack2, haystack1Length)) + ignoreCase + ? Character.toLowerCase(charAt(ii, haystack1, haystack2, haystack1Length)) : charAt(ii, haystack1, haystack2, haystack1Length); char c2 = ignoreCase ? Character.toLowerCase(needle.charAt(j)) : needle.charAt(j); if (c != c2) { @@ -211,7 +231,6 @@ static char charAt(int i, CharSequence firstPart, CharSequence secondPart, int f return i < firstPartLength ? firstPart.charAt(i) : secondPart.charAt(i - firstPartLength); } - /** * Checks if the given string matches the wildcard pattern. * @@ -221,17 +240,15 @@ static char charAt(int i, CharSequence firstPart, CharSequence secondPart, int f public abstract boolean matches(CharSequence s); /** - * This is a different version of {@link #matches(CharSequence)} which has the same semantics as calling - * {@code matcher.matches(firstPart + secondPart);}. - *

    - * The difference is that this method does not allocate memory. - *

    + * This is a different version of {@link #matches(CharSequence)} which has the same semantics as + * calling {@code matcher.matches(firstPart + secondPart);}. + * + *

    The difference is that this method does not allocate memory. * * @param firstPart The first part of the string to match against. * @param secondPart The second part of the string to match against. - * @return {@code true}, - * when the wildcard pattern matches the partitioned string, - * {@code false} otherwise. + * @return {@code true}, when the wildcard pattern matches the partitioned string, {@code false} + * otherwise. */ public abstract boolean matches(CharSequence firstPart, @Nullable CharSequence secondPart); @@ -246,16 +263,16 @@ public boolean equals(Object obj) { public abstract String getMatcher(); /** - * This {@link WildcardMatcher} supports wildcards in the middle of the matcher by decomposing the matcher into several - * {@link SimpleWildcardMatcher}s. + * This {@link WildcardMatcher} supports wildcards in the middle of the matcher by decomposing the + * matcher into several {@link SimpleWildcardMatcher}s. */ static class CompoundWildcardMatcher extends WildcardMatcher { private final String wildcardString; private final String matcher; private final List wildcardMatchers; - CompoundWildcardMatcher(String wildcardString, String matcher, - List wildcardMatchers) { + CompoundWildcardMatcher( + String wildcardString, String matcher, List wildcardMatchers) { this.wildcardString = wildcardString; this.matcher = matcher; this.wildcardMatchers = wildcardMatchers; @@ -300,9 +317,7 @@ public String getMatcher() { } } - /** - * This {@link} does not support wildcards in the middle of a matcher. - */ + /** This {@link} does not support wildcards in the middle of a matcher. */ static class SimpleWildcardMatcher extends WildcardMatcher { private final String matcher; @@ -311,19 +326,23 @@ static class SimpleWildcardMatcher extends WildcardMatcher { private final boolean wildcardAtBeginning; private final boolean ignoreCase; - SimpleWildcardMatcher(String matcher, boolean wildcardAtBeginning, boolean wildcardAtEnd, - boolean ignoreCase) { + SimpleWildcardMatcher( + String matcher, boolean wildcardAtBeginning, boolean wildcardAtEnd, boolean ignoreCase) { this.matcher = matcher; this.wildcardAtEnd = wildcardAtEnd; this.wildcardAtBeginning = wildcardAtBeginning; this.ignoreCase = ignoreCase; - this.stringRepresentation = new StringBuilder( - matcher.length() + CASE_SENSITIVE_PREFIX.length() + WILDCARD.length() + WILDCARD.length()) - .append(ignoreCase ? "" : CASE_SENSITIVE_PREFIX) - .append(wildcardAtBeginning ? WILDCARD : "") - .append(matcher) - .append(wildcardAtEnd ? WILDCARD : "") - .toString(); + this.stringRepresentation = + new StringBuilder( + matcher.length() + + CASE_SENSITIVE_PREFIX.length() + + WILDCARD.length() + + WILDCARD.length()) + .append(ignoreCase ? "" : CASE_SENSITIVE_PREFIX) + .append(wildcardAtBeginning ? WILDCARD : "") + .append(matcher) + .append(wildcardAtEnd ? WILDCARD : "") + .toString(); } @Override @@ -355,8 +374,13 @@ int indexOf(CharSequence firstPart, @Nullable CharSequence secondPart, int offse } else if (wildcardAtEnd) { return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, 0, 1); } else if (wildcardAtBeginning) { - return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, - totalLength - matcher.length(), totalLength); + return indexOfIgnoreCase( + firstPart, + secondPart, + matcher, + ignoreCase, + totalLength - matcher.length(), + totalLength); } else if (totalLength == matcher.length()) { return indexOfIgnoreCase(firstPart, secondPart, matcher, ignoreCase, 0, totalLength); } else { @@ -370,4 +394,3 @@ public String getMatcher() { } } } - diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java index f478e3b67..31af5cee7 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java @@ -52,7 +52,6 @@ public final void recycle(T obj) { } } - public final long getGarbageCreated() { return garbageCreated.longValue(); } @@ -63,7 +62,7 @@ public final long getGarbageCreated() { * @param obj recycled object to return to pool * @return true if object has been returned to pool, false if pool is already full */ - abstract protected boolean returnToPool(T obj); + protected abstract boolean returnToPool(T obj); /** * Tries to create an instance in pool @@ -71,5 +70,5 @@ public final long getGarbageCreated() { * @return {@code null} if pool capacity is exhausted */ @Nullable - abstract protected T tryCreateInstance(); + protected abstract T tryCreateInstance(); } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java index 2afb8dec8..194a9f8fa 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.pooling; /** diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java index 0ff5eaaa3..81455c660 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.pooling; import org.jctools.queues.MpmcArrayQueue; @@ -5,13 +23,15 @@ /** * Object pool * - * @param pooled object type. Does not have to implement {@link Recyclable} in order to allow for dealing with objects - * that are outside of elastic apm agent (like standard JDK or third party library classes). + * @param pooled object type. Does not have to implement {@link Recyclable} in order to allow + * for dealing with objects that are outside of elastic apm agent (like standard JDK or third + * party library classes). */ public interface ObjectPool { /** - * Tries to reuse any existing instance if pool has any, otherwise creates a new un-pooled instance + * Tries to reuse any existing instance if pool has any, otherwise creates a new un-pooled + * instance * * @return object instance, either from pool or freshly allocated */ @@ -26,9 +46,8 @@ public interface ObjectPool { void clear(); - public static ObjectPool createRecyclable(int capacity, - Allocator allocator) { + public static ObjectPool createRecyclable( + int capacity, Allocator allocator) { return QueueBasedObjectPool.ofRecyclable(new MpmcArrayQueue<>(capacity), false, allocator); } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java index 6399fbd8f..fadfbe223 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java @@ -1,6 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.pooling; - import java.util.Queue; import javax.annotation.Nullable; @@ -9,37 +26,44 @@ public class QueueBasedObjectPool extends AbstractObjectPool { private final Queue queue; /** - * Creates a queue based pooled for types that implement {@link Recyclable}, use {@link #of(Queue, boolean, Allocator, Resetter)} - * for other pooled object types. + * Creates a queue based pooled for types that implement {@link Recyclable}, use {@link #of(Queue, + * boolean, Allocator, Resetter)} for other pooled object types. * * @param queue the underlying queue * @param preAllocate when set to true, queue will be be pre-allocated with object instance. - * @param allocator a factory used to create new instances of the recyclable object. This factory is used when - * there are no objects in the queue and to preallocate the queue + * @param allocator a factory used to create new instances of the recyclable object. This factory + * is used when there are no objects in the queue and to preallocate the queue */ - public static QueueBasedObjectPool ofRecyclable(Queue queue, - boolean preAllocate, Allocator allocator) { - return new QueueBasedObjectPool<>(queue, preAllocate, allocator, - Resetter.ForRecyclable.get()); + public static QueueBasedObjectPool ofRecyclable( + Queue queue, boolean preAllocate, Allocator allocator) { + return new QueueBasedObjectPool<>( + queue, preAllocate, allocator, Resetter.ForRecyclable.get()); } /** - * Creates a queue based pooled for types that do not implement {@link Recyclable}, use {@link #ofRecyclable(Queue, boolean, Allocator)} - * for types that implement {@link Recyclable}. + * Creates a queue based pooled for types that do not implement {@link Recyclable}, use {@link + * #ofRecyclable(Queue, boolean, Allocator)} for types that implement {@link Recyclable}. * * @param queue the underlying queue - * @param preAllocate when set to true, queue will be be pre-allocated with object instances fitting queue size - * @param allocator a factory used to create new instances of the recyclable object. This factory is used when - * there are no objects in the queue and to preallocate the queue + * @param preAllocate when set to true, queue will be be pre-allocated with object instances + * fitting queue size + * @param allocator a factory used to create new instances of the recyclable object. This factory + * is used when there are no objects in the queue and to preallocate the queue * @param resetter a reset strategy class */ - public static QueueBasedObjectPool of(Queue queue, boolean preAllocate, - Allocator allocator, Resetter resetter) { + public static QueueBasedObjectPool of( + Queue queue, + boolean preAllocate, + Allocator allocator, + Resetter resetter) { return new QueueBasedObjectPool<>(queue, preAllocate, allocator, resetter); } - private QueueBasedObjectPool(Queue queue, boolean preAllocate, - Allocator allocator, Resetter resetter) { + private QueueBasedObjectPool( + Queue queue, + boolean preAllocate, + Allocator allocator, + Resetter resetter) { super(allocator, resetter); this.queue = queue; if (preAllocate) { @@ -70,5 +94,4 @@ public int getObjectsInPool() { public void clear() { queue.clear(); } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java index 02f453ff6..26ce10fce 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java @@ -1,10 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.pooling; public interface Recyclable { - /** - * resets pooled object state so it can be reused - */ + /** resets pooled object state so it can be reused */ void resetState(); - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java index d848f2153..f68e7f088 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.pooling; /** @@ -32,5 +50,4 @@ public void recycle(Recyclable object) { object.resetState(); } } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java index 3bc715b2b..853ae9dd9 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.util; public class ByteUtils { diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java index 98162d43e..f6a30d3de 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.util; public class HexUtils { @@ -53,7 +71,6 @@ public static long hexToLong(CharSequence hex, int offset) { | hexCharToBinary(hex.charAt(offset + 15)); } - private static long hexCharToBinary(char ch) { if ('0' <= ch && ch <= '9') { return ch - '0'; @@ -66,5 +83,4 @@ private static long hexCharToBinary(char ch) { } throw new IllegalArgumentException("Not a hex char: " + ch); } - } diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java index b4a72c4ca..e8acc3ffe 100644 --- a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java +++ b/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.util; import java.lang.invoke.MethodHandle; @@ -9,7 +27,6 @@ public class ThreadUtils { private static final MethodHandle VIRTUAL_CHECKER = generateVirtualChecker(); - public static boolean isVirtual(Thread thread) { try { return (boolean) VIRTUAL_CHECKER.invokeExact(thread); @@ -23,14 +40,13 @@ private static MethodHandle generateVirtualChecker() { try { isVirtual = Thread.class.getMethod("isVirtual"); isVirtual.invoke( - Thread.currentThread()); //invoke to ensure it does not throw exceptions for preview versions + Thread.currentThread()); // invoke to ensure it does not throw exceptions for preview + // versions return MethodHandles.lookup().unreflect(isVirtual); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - //virtual threads are not supported, therefore no thread is virtual + // virtual threads are not supported, therefore no thread is virtual return MethodHandles.dropArguments( - MethodHandles.constant(boolean.class, false), - 0, - Thread.class); + MethodHandles.constant(boolean.class, false), 0, Thread.class); } } } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java index 4ac2d898a..4e483894c 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java @@ -18,7 +18,6 @@ */ package co.elastic.apm.otel.profiler; - import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import co.elastic.apm.otel.profiler.pooling.ObjectPool; @@ -43,7 +42,8 @@ class CallTreeSpanifyTest { static { - //we can't reset context storage wrappers between tests, so we msut ensure that it is registered before we create ANY Otel instance + // we can't reset context storage wrappers between tests, so we msut ensure that it is + // registered before we create ANY Otel instance ProfilingActivationListener.ensureInitialized(); } @@ -52,23 +52,21 @@ class CallTreeSpanifyTest { @DisabledOnAppleSilicon void testSpanification() throws Exception { FixedNanoClock nanoClock = new FixedNanoClock(); - try (ProfilerTestSetup setup = ProfilerTestSetup.create(config -> config - .clock(nanoClock) - .startScheduledProfiling(false) - )) { + try (ProfilerTestSetup setup = + ProfilerTestSetup.create( + config -> config.clock(nanoClock).startScheduledProfiling(false))) { setup.profiler.setProfilingSessionOngoing(true); - CallTree.Root callTree = CallTreeTest.getCallTree(setup, new String[] { - " dd ", - " cc ", - " bbb ", - "aaaaee" - }); + CallTree.Root callTree = + CallTreeTest.getCallTree(setup, new String[] {" dd ", " cc ", " bbb ", "aaaaee"}); assertThat(callTree.spanify(nanoClock, setup.sdk.getTracer("dummy-tracer"))).isEqualTo(4); assertThat(setup.getSpans()).hasSize(5); - assertThat(setup.getSpans().stream() - .map(SpanData::getName) - ).containsExactly("Call Tree Root", "CallTreeTest#a", "CallTreeTest#b", - "CallTreeTest#d", "CallTreeTest#e"); + assertThat(setup.getSpans().stream().map(SpanData::getName)) + .containsExactly( + "Call Tree Root", + "CallTreeTest#a", + "CallTreeTest#b", + "CallTreeTest#d", + "CallTreeTest#e"); SpanData a = setup.getSpans().get(1); assertThat(a).hasName("CallTreeTest#a"); @@ -91,7 +89,6 @@ void testSpanification() throws Exception { assertThat(e.getEndEpochNanos() - e.getStartEpochNanos()).isEqualTo(10_000_000); assertThat(e.getAttributes().get(CallTree.STACKTRACE_ATTRIBUTE_KEY)).isBlank(); } - } @Test @@ -100,39 +97,42 @@ void testCallTreeWithActiveSpan() { String traceId = "0af7651916cd43dd8448eb211c80319c"; String rootSpanId = "b7ad6b7169203331"; - TraceContext rootContext = TraceContext.fromSpanContextWithZeroClockAnchor(SpanContext.create( - traceId, - rootSpanId, - TraceFlags.getSampled(), - TraceState.getDefault() - )); + TraceContext rootContext = + TraceContext.fromSpanContextWithZeroClockAnchor( + SpanContext.create( + traceId, rootSpanId, TraceFlags.getSampled(), TraceState.getDefault())); ObjectPool rootPool = ObjectPool.createRecyclable(2, CallTree.Root::new); ObjectPool childPool = ObjectPool.createRecyclable(2, CallTree::new); - CallTree.Root root = CallTree.createRoot(rootPool, - rootContext.serialize(), 0); + CallTree.Root root = CallTree.createRoot(rootPool, rootContext.serialize(), 0); root.addStackTrace(Collections.singletonList(StackFrame.of("A", "a")), 0, childPool, 0); String childSpanId = "a1b2c3d4e5f64242"; - TraceContext spanContext = TraceContext.fromSpanContextWithZeroClockAnchor(SpanContext.create( - traceId, - childSpanId, - TraceFlags.getSampled(), - TraceState.getDefault() - )); + TraceContext spanContext = + TraceContext.fromSpanContextWithZeroClockAnchor( + SpanContext.create( + traceId, childSpanId, TraceFlags.getSampled(), TraceState.getDefault())); root.onActivation(spanContext.serialize(), TimeUnit.MILLISECONDS.toNanos(5)); - root.addStackTrace(Arrays.asList(StackFrame.of("A", "b"), StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(10), childPool, 0); - root.addStackTrace(Arrays.asList(StackFrame.of("A", "b"), StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(20), childPool, 0); - root.onDeactivation(spanContext.serialize(), rootContext.serialize(), - TimeUnit.MILLISECONDS.toNanos(25)); - - root.addStackTrace(Collections.singletonList(StackFrame.of("A", "a")), + root.addStackTrace( + Arrays.asList(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(10), + childPool, + 0); + root.addStackTrace( + Arrays.asList(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(20), + childPool, + 0); + root.onDeactivation( + spanContext.serialize(), rootContext.serialize(), TimeUnit.MILLISECONDS.toNanos(25)); + + root.addStackTrace( + Collections.singletonList(StackFrame.of("A", "a")), TimeUnit.MILLISECONDS.toNanos(30), - childPool, 0); + childPool, + 0); root.end(childPool, 0); System.out.println(root); @@ -156,24 +156,20 @@ void testCallTreeWithActiveSpan() { assertThat(b.getChildren()).isEmpty(); InMemorySpanExporter exporter = InMemorySpanExporter.create(); - OpenTelemetrySdkBuilder sdkBuilder = OpenTelemetrySdk.builder() - .setTracerProvider(SdkTracerProvider.builder() - .addSpanProcessor(SimpleSpanProcessor.create(exporter)) - .build()); + OpenTelemetrySdkBuilder sdkBuilder = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build()); try (OpenTelemetrySdk outputSdk = sdkBuilder.build()) { root.spanify(nanoClock, outputSdk.getTracer("dummy-tracer")); List spans = exporter.getFinishedSpanItems(); assertThat(spans).hasSize(2); - assertThat(spans.get(0)) - .hasTraceId(traceId) - .hasParentSpanId(rootSpanId); - assertThat(spans.get(1)) - .hasTraceId(traceId) - .hasParentSpanId(childSpanId); + assertThat(spans.get(0)).hasTraceId(traceId).hasParentSpanId(rootSpanId); + assertThat(spans.get(1)).hasTraceId(traceId).hasParentSpanId(childSpanId); } - } - } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java index 2294b6278..a6c7aae35 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java @@ -61,8 +61,8 @@ class CallTreeTest { void setUp() { nanoClock = new FixedNanoClock(); // disable scheduled profiling to not interfere with this test - profilerSetup = ProfilerTestSetup.create( - config -> config.clock(nanoClock).startScheduledProfiling(false)); + profilerSetup = + ProfilerTestSetup.create(config -> config.clock(nanoClock).startScheduledProfiling(false)); profilerSetup.profiler.setProfilingSessionOngoing(true); } @@ -74,16 +74,23 @@ void tearDown() throws IOException { @Test void testCallTree() { TraceContext traceContext = new TraceContext(); - CallTree.Root root = CallTree.createRoot( - ObjectPool.createRecyclable(100, CallTree.Root::new), traceContext.serialize(), 0); + CallTree.Root root = + CallTree.createRoot( + ObjectPool.createRecyclable(100, CallTree.Root::new), traceContext.serialize(), 0); ObjectPool callTreePool = ObjectPool.createRecyclable(100, CallTree::new); root.addStackTrace(List.of(StackFrame.of("A", "a")), 0, callTreePool, 0); - root.addStackTrace(List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(10), callTreePool, 0); - root.addStackTrace(List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), - TimeUnit.MILLISECONDS.toNanos(20), callTreePool, 0); - root.addStackTrace(List.of(StackFrame.of("A", "a")), TimeUnit.MILLISECONDS.toNanos(30), - callTreePool, 0); + root.addStackTrace( + List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(10), + callTreePool, + 0); + root.addStackTrace( + List.of(StackFrame.of("A", "b"), StackFrame.of("A", "a")), + TimeUnit.MILLISECONDS.toNanos(20), + callTreePool, + 0); + root.addStackTrace( + List.of(StackFrame.of("A", "a")), TimeUnit.MILLISECONDS.toNanos(30), callTreePool, 0); root.end(callTreePool, 0); System.out.println(root); @@ -127,130 +134,113 @@ void testGiveEmptyChildIdsTo() { assertThat(poor.hasChildIds()).isTrue(); } - @Test void testTwoDistinctInvocationsOfMethodBShouldNotBeFoldedIntoOne() throws Exception { - assertCallTree(new String[] { - " bb bb", - "aaaaaa" - }, new Object[][] { - {"a", 6}, - {" b", 2}, - {" b", 2} - }); + assertCallTree( + new String[] {" bb bb", "aaaaaa"}, + new Object[][] { + {"a", 6}, + {" b", 2}, + {" b", 2} + }); } - @Test void testBasicCallTree() throws Exception { - assertCallTree(new String[] { - " cc ", - " bbb", - "aaaa" - }, new Object[][] { - {"a", 4}, - {" b", 3}, - {" c", 2} - }, new Object[][] { - {"a", 3}, - {" b", 2}, - {" c", 1} - }); + assertCallTree( + new String[] {" cc ", " bbb", "aaaa"}, + new Object[][] { + {"a", 4}, + {" b", 3}, + {" c", 2} + }, + new Object[][] { + {"a", 3}, + {" b", 2}, + {" c", 1} + }); } @Test void testShouldNotCreateInferredSpansForPillarsAndLeafShouldHaveStacktrace() throws Exception { - assertCallTree(new String[] { - " dd ", - " cc ", - " bb ", - "aaaa" - }, new Object[][] { - {"a", 4}, - {" b", 2}, - {" c", 2}, - {" d", 2} - }, new Object[][] { - {"a", 3}, - {" d", 1, List.of("c", "b")} - }); + assertCallTree( + new String[] {" dd ", " cc ", " bb ", "aaaa"}, + new Object[][] { + {"a", 4}, + {" b", 2}, + {" c", 2}, + {" d", 2} + }, + new Object[][] { + {"a", 3}, + {" d", 1, List.of("c", "b")} + }); } @Test void testRemoveNodesWithCountOne() throws Exception { - assertCallTree(new String[] { - " b ", - "aaa" - }, new Object[][] { - {"a", 3} - }, new Object[][] { - {"a", 2} - }); + assertCallTree( + new String[] {" b ", "aaa"}, new Object[][] {{"a", 3}}, new Object[][] {{"a", 2}}); } @Test void testSameTopOfStackDifferentBottom() throws Exception { - assertCallTree(new String[] { - "cccc", - "aabb" - }, new Object[][] { - {"a", 2}, - {" c", 2}, - {"b", 2}, - {" c", 2}, - }); + assertCallTree( + new String[] {"cccc", "aabb"}, + new Object[][] { + {"a", 2}, + {" c", 2}, + {"b", 2}, + {" c", 2}, + }); } @Test void testStackTraceWithRecursion() throws Exception { - assertCallTree(new String[] { - "bbccbbcc", - "bbbbbbbb", - "aaaaaaaa" - }, new Object[][] { - {"a", 8}, - {" b", 8}, - {" b", 2}, - {" c", 2}, - {" b", 2}, - {" c", 2}, - }); + assertCallTree( + new String[] {"bbccbbcc", "bbbbbbbb", "aaaaaaaa"}, + new Object[][] { + {"a", 8}, + {" b", 8}, + {" b", 2}, + {" c", 2}, + {" b", 2}, + {" c", 2}, + }); } @Test void testFirstInferredSpanShouldHaveNoStackTrace() throws Exception { - assertCallTree(new String[] { - "bb", - "aa" - }, new Object[][] { - {"a", 2}, - {" b", 2}, - }, new Object[][] { - {"b", 1}, - }); + assertCallTree( + new String[] {"bb", "aa"}, + new Object[][] { + {"a", 2}, + {" b", 2}, + }, + new Object[][] { + {"b", 1}, + }); } @Test void testCallTreeWithSpanActivations() throws Exception { - assertCallTree(new String[] { - " cc ee ", - " bbb dd ", - " a aaaaaa a ", - "1 2 2 1" - }, new Object[][] { - {"a", 8}, - {" b", 3}, - {" c", 2}, - {" d", 2}, - {" e", 2}, - }, new Object[][] { - {"1", 11}, - {" a", 9}, - {" 2", 7}, - {" b", 2}, - {" c", 1}, - {" e", 1, List.of("d")}, - }); + assertCallTree( + new String[] {" cc ee ", " bbb dd ", " a aaaaaa a ", "1 2 2 1"}, + new Object[][] { + {"a", 8}, + {" b", 3}, + {" c", 2}, + {" d", 2}, + {" e", 2}, + }, + new Object[][] { + {"1", 11}, + {" a", 9}, + {" 2", 7}, + {" b", 2}, + {" c", 1}, + {" e", 1, List.of("d")}, + }); } /* @@ -263,25 +253,28 @@ void testCallTreeWithSpanActivations() throws Exception { */ @Test void testDeactivationBeforeEnd() throws Exception { - assertCallTree(new String[] { - " dd ", - " cccc c ", - " bbbb bb ", // <- deactivation for span 2 happens before b and c ends - " a aaaa aa ", // that means b and c must have started before 2 has been activated - "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 - }, new Object[][] { - {"a", 7}, - {" b", 6}, - {" c", 5}, - {" d", 2}, - }, new Object[][] { - {"1", 10}, - {" a", 8}, - {" b", 7}, - {" c", 6}, - {" 2", 5}, - {" d", 1}, - }); + assertCallTree( + new String[] { + " dd ", + " cccc c ", + " bbbb bb ", // <- deactivation for span 2 happens before b and c ends + " a aaaa aa ", // that means b and c must have started before 2 has been activated + "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 + }, + new Object[][] { + {"a", 7}, + {" b", 6}, + {" c", 5}, + {" d", 2}, + }, + new Object[][] { + {"1", 10}, + {" a", 8}, + {" b", 7}, + {" c", 6}, + {" 2", 5}, + {" d", 1}, + }); } /* @@ -292,20 +285,19 @@ void testDeactivationBeforeEnd() throws Exception { */ @Test void testDectivationBeforeEnd2() throws Exception { - assertCallTree(new String[] { - " bbbb b ", - " a aaaa a a a ", - "1 2 2 3 3 1" - }, new Object[][] { - {"a", 8}, - {" b", 5}, - }, new Object[][] { - {"1", 13}, - {" a", 11}, - {" b", 6}, - {" 2", 5}, - {" 3", 2}, - }); + assertCallTree( + new String[] {" bbbb b ", " a aaaa a a a ", "1 2 2 3 3 1"}, + new Object[][] { + {"a", 8}, + {" b", 5}, + }, + new Object[][] { + {"1", 13}, + {" a", 11}, + {" b", 6}, + {" 2", 5}, + {" 3", 2}, + }); } /* @@ -317,21 +309,20 @@ void testDectivationBeforeEnd2() throws Exception { */ @Test void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations() throws Exception { - Map spans = assertCallTree(new String[] { - " c c ", - " b b ", - "a a a aa", - " 1 1 2 2 " - }, new Object[][] { - {"a", 5}, - {" b", 2}, - {" c", 2}, - }, new Object[][] { - {"a", 9}, - {" 1", 2}, - {" c", 3, List.of("b")}, - {" 2", 2}, - }); + Map spans = + assertCallTree( + new String[] {" c c ", " b b ", "a a a aa", " 1 1 2 2 "}, + new Object[][] { + {"a", 5}, + {" b", 2}, + {" c", 2}, + }, + new Object[][] { + {"a", 9}, + {" 1", 2}, + {" c", 3, List.of("b")}, + {" 2", 2}, + }); assertThat(spans.get("a").getLinks()) .hasSize(1) .anySatisfy( @@ -351,22 +342,21 @@ void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations() throws E */ @Test void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations_Nested() throws Exception { - Map spans = assertCallTree(new String[] { - " c c ", - " b b ", - "a a a aa", - " 1 1 23 32 " - }, new Object[][] { - {"a", 5}, - {" b", 2}, - {" c", 2}, - }, new Object[][] { - {"a", 11}, - {" 1", 2}, - {" c", 4, List.of("b")}, - {" 2", 4}, - {" 3", 2}, - }); + Map spans = + assertCallTree( + new String[] {" c c ", " b b ", "a a a aa", " 1 1 23 32 "}, + new Object[][] { + {"a", 5}, + {" b", 2}, + {" c", 2}, + }, + new Object[][] { + {"a", 11}, + {" 1", 2}, + {" c", 4, List.of("b")}, + {" 2", 4}, + {" 3", 2}, + }); assertThat(spans.get("a").getLinks()) .hasSize(1) .anySatisfy( @@ -383,18 +373,17 @@ void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations_Nested() t */ @Test void testActivationAfterMethodEnds() throws Exception { - assertCallTree(new String[] { - "bb ", - "aa a ", - " 1 1" - }, new Object[][] { - {"a", 3}, - {" b", 2}, - }, new Object[][] { - {"a", 3}, - {" b", 1}, - {" 1", 2} - }); + assertCallTree( + new String[] {"bb ", "aa a ", " 1 1"}, + new Object[][] { + {"a", 3}, + {" b", 2}, + }, + new Object[][] { + {"a", 3}, + {" b", 1}, + {" 1", 2} + }); } /* @@ -403,18 +392,17 @@ void testActivationAfterMethodEnds() throws Exception { */ @Test void testActivationBetweenMethods() throws Exception { - assertCallTree(new String[] { - "bb ", - "aa a", - " 11 " - }, new Object[][] { - {"a", 3}, - {" b", 2}, - }, new Object[][] { - {"a", 4}, - {" b", 1}, - {" 1", 1}, - }); + assertCallTree( + new String[] {"bb ", "aa a", " 11 "}, + new Object[][] { + {"a", 3}, + {" b", 2}, + }, + new Object[][] { + {"a", 4}, + {" b", 1}, + {" 1", 1}, + }); } /* @@ -424,19 +412,17 @@ void testActivationBetweenMethods() throws Exception { */ @Test void testActivationBetweenMethods_AfterFastMethod() throws Exception { - assertCallTree(new String[] { - " c ", - "bb ", - "aa a", - " 11 " - }, new Object[][] { - {"a", 3}, - {" b", 2}, - }, new Object[][] { - {"a", 4}, - {" b", 1}, - {" 1", 1}, - }); + assertCallTree( + new String[] {" c ", "bb ", "aa a", " 11 "}, + new Object[][] { + {"a", 3}, + {" b", 2}, + }, + new Object[][] { + {"a", 4}, + {" b", 1}, + {" 1", 1}, + }); } /* @@ -446,46 +432,46 @@ void testActivationBetweenMethods_AfterFastMethod() throws Exception { */ @Test void testActivationBetweenFastMethods() throws Exception { - assertCallTree(new String[] { - "c d ", - "b b ", - "a a a", - " 11 22 " - }, new Object[][] { - {"a", 3}, - {" b", 2}, - }, new Object[][] { - {"a", 6}, - {" b", 3}, - {" 1", 1}, - {" 2", 1}, - }); + assertCallTree( + new String[] {"c d ", "b b ", "a a a", " 11 22 "}, + new Object[][] { + {"a", 3}, + {" b", 2}, + }, + new Object[][] { + {"a", 6}, + {" b", 3}, + {" 1", 1}, + {" 2", 1}, + }); } - /* *//* + /* */ + /* * [a ] * [b] [1 [c] - *//* - @Test - void testActivationBetweenMethods_WithCommonAncestor() throws Exception { - assertCallTree(new String[]{ - " c f g ", - "bbb e d dd", - "aaa a a aa", - " 11 22 33 " - }, new Object[][] { - {"a", 7}, - {" b", 3}, - {" d", 3}, - }, new Object[][] { - {"a", 12}, - {" b", 2}, - {" 1", 1}, - {" 2", 1}, - {" d", 4}, - {" 3", 1}, - }); - }*/ + */ + /* + @Test + void testActivationBetweenMethods_WithCommonAncestor() throws Exception { + assertCallTree(new String[]{ + " c f g ", + "bbb e d dd", + "aaa a a aa", + " 11 22 33 " + }, new Object[][] { + {"a", 7}, + {" b", 3}, + {" d", 3}, + }, new Object[][] { + {"a", 12}, + {" b", 2}, + {" 1", 1}, + {" 2", 1}, + {" d", 4}, + {" 3", 1}, + }); + }*/ /* * [a ] @@ -494,16 +480,16 @@ void testActivationBetweenMethods_WithCommonAncestor() throws Exception { */ @Test void testNestedActivation() throws Exception { - assertCallTree(new String[] { - "a a a", - " 12 21 " - }, new Object[][] { - {"a", 3}, - }, new Object[][] { - {"a", 6}, - {" 1", 4}, - {" 2", 2}, - }); + assertCallTree( + new String[] {"a a a", " 12 21 "}, + new Object[][] { + {"a", 3}, + }, + new Object[][] { + {"a", 6}, + {" 1", 4}, + {" 2", 2}, + }); } /* @@ -514,21 +500,21 @@ void testNestedActivation() throws Exception { */ @Test void testNestedActivationAfterMethodEnds_RootChangesToC() throws Exception { - Map spans = assertCallTree(new String[] { - " bbb ", - " aaa ccc ", - "1 23 321" - }, new Object[][] { - {"a", 3}, - {" b", 3}, - {"c", 3}, - }, new Object[][] { - {"1", 11}, - {" b", 2, List.of("a")}, - {" 2", 6}, - {" 3", 4}, - {" c", 2} - }); + Map spans = + assertCallTree( + new String[] {" bbb ", " aaa ccc ", "1 23 321"}, + new Object[][] { + {"a", 3}, + {" b", 3}, + {"c", 3}, + }, + new Object[][] { + {"1", 11}, + {" b", 2, List.of("a")}, + {" 2", 6}, + {" 3", 4}, + {" c", 2} + }); assertThat(spans.get("b").getLinks()).isEmpty(); } @@ -541,23 +527,21 @@ void testNestedActivationAfterMethodEnds_RootChangesToC() throws Exception { */ @Test void testRegularActivationFollowedByNestedActivationAfterMethodEnds() throws Exception { - assertCallTree(new String[] { - " d ", - " b b b ", - " a a a ccc ", - "1 2 2 34 431" - }, new Object[][] { - {"a", 3}, - {" b", 3}, - {"c", 3}, - }, new Object[][] { - {"1", 13}, - {" b", 4, List.of("a")}, - {" 2", 2}, - {" 3", 6}, - {" 4", 4}, - {" c", 2} - }); + assertCallTree( + new String[] {" d ", " b b b ", " a a a ccc ", "1 2 2 34 431"}, + new Object[][] { + {"a", 3}, + {" b", 3}, + {"c", 3}, + }, + new Object[][] { + {"1", 13}, + {" b", 4, List.of("a")}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} + }); } /* @@ -569,43 +553,45 @@ void testRegularActivationFollowedByNestedActivationAfterMethodEnds() throws Exc */ @Test void testNestedActivationAfterMethodEnds_CommonAncestorA() throws Exception { - Map spans = assertCallTree(new String[] { - " b b b ccc ", - " aa a a aaa a ", - "1 2 2 34 43 1" - }, new Object[][] { - {"a", 8}, - {" b", 3}, - {" c", 3}, - }, new Object[][] { - {"1", 15}, - {" a", 13}, - {" b", 4}, - {" 2", 2}, - {" 3", 6}, - {" 4", 4}, - {" c", 2} - }); + Map spans = + assertCallTree( + new String[] {" b b b ccc ", " aa a a aaa a ", "1 2 2 34 43 1"}, + new Object[][] { + {"a", 8}, + {" b", 3}, + {" c", 3}, + }, + new Object[][] { + {"1", 15}, + {" a", 13}, + {" b", 4}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} + }); assertThat(spans.get("b").getLinks()) .hasSize(1) - .anySatisfy(link -> { - assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); - SpanData expectedSpan = spans.get("2"); - assertThat(link.getSpanContext().getTraceId()).isEqualTo(expectedSpan.getTraceId()); - assertThat(link.getSpanContext().getSpanId()).isEqualTo(expectedSpan.getSpanId()); - }); + .anySatisfy( + link -> { + assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); + SpanData expectedSpan = spans.get("2"); + assertThat(link.getSpanContext().getTraceId()).isEqualTo(expectedSpan.getTraceId()); + assertThat(link.getSpanContext().getSpanId()).isEqualTo(expectedSpan.getSpanId()); + }); assertThat(spans.get("c").getLinks()).isEmpty(); assertThat(spans.get("a").getLinks()) .hasSize(1) - .anySatisfy(link -> { - assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); - SpanData expectedSpan = spans.get("3"); - assertThat(link.getSpanContext().getTraceId()).isEqualTo(expectedSpan.getTraceId()); - assertThat(link.getSpanContext().getSpanId()).isEqualTo(expectedSpan.getSpanId()); - }); + .anySatisfy( + link -> { + assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); + SpanData expectedSpan = spans.get("3"); + assertThat(link.getSpanContext().getTraceId()).isEqualTo(expectedSpan.getTraceId()); + assertThat(link.getSpanContext().getSpanId()).isEqualTo(expectedSpan.getSpanId()); + }); } /* @@ -617,20 +603,19 @@ void testNestedActivationAfterMethodEnds_CommonAncestorA() throws Exception { */ @Test void testActivationAfterMethodEnds_RootChangesToB() throws Exception { - assertCallTree(new String[] { - " ccc ", - " aaa bbb ", - "1 2 21" - }, new Object[][] { - {"a", 3}, - {"b", 3}, - {" c", 3}, - }, new Object[][] { - {"1", 9}, - {" a", 2}, - {" 2", 4}, - {" c", 2, List.of("b")} - }); + assertCallTree( + new String[] {" ccc ", " aaa bbb ", "1 2 21"}, + new Object[][] { + {"a", 3}, + {"b", 3}, + {" c", 3}, + }, + new Object[][] { + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" c", 2, List.of("b")} + }); } /* @@ -641,37 +626,35 @@ void testActivationAfterMethodEnds_RootChangesToB() throws Exception { */ @Test void testActivationAfterMethodEnds_RootChangesToB2() throws Exception { - assertCallTree(new String[] { - " aaa bbb ", - "1 2 21" - }, new Object[][] { - {"a", 3}, - {"b", 3}, - }, new Object[][] { - {"1", 9}, - {" a", 2}, - {" 2", 4}, - {" b", 2} - }); - } - - - /* - * [1] - * [a] - @Test - void testActivationBeforeCallTree() throws Exception { - assertCallTree(new String[]{ - " aaa", - "1 1 " - }, new Object[][] { - {"a", 3}, - }, new Object[][] { - {"a", 3}, - {" 1", 2}, + assertCallTree( + new String[] {" aaa bbb ", "1 2 21"}, + new Object[][] { + {"a", 3}, + {"b", 3}, + }, + new Object[][] { + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" b", 2} }); - } */ + } + /* + * [1] + * [a] + @Test + void testActivationBeforeCallTree() throws Exception { + assertCallTree(new String[]{ + " aaa", + "1 1 " + }, new Object[][] { + {"a", 3}, + }, new Object[][] { + {"a", 3}, + {" 1", 2}, + }); + } */ /* * [1 ] @@ -682,19 +665,18 @@ void testActivationBeforeCallTree() throws Exception { */ @Test void testActivationAfterMethodEnds_SameRootDeeperStack() throws Exception { - assertCallTree(new String[] { - " ccc ", - " aaa aaa ", - "1 2 21" - }, new Object[][] { - {"a", 6}, - {" c", 3}, - }, new Object[][] { - {"1", 9}, - {" a", 6}, - {" 2", 4}, - {" c", 2} - }); + assertCallTree( + new String[] {" ccc ", " aaa aaa ", "1 2 21"}, + new Object[][] { + {"a", 6}, + {" c", 3}, + }, + new Object[][] { + {"1", 9}, + {" a", 6}, + {" 2", 4}, + {" c", 2} + }); } /* @@ -705,19 +687,18 @@ void testActivationAfterMethodEnds_SameRootDeeperStack() throws Exception { */ @Test void testActivationBeforeMethodStarts() throws Exception { - assertCallTree(new String[] { - " bbb ", - " a aaa a ", - "1 2 2 1" - }, new Object[][] { - {"a", 5}, - {" b", 3}, - }, new Object[][] { - {"1", 8}, - {" a", 6}, - {" 2", 4}, - {" b", 2} - }); + assertCallTree( + new String[] {" bbb ", " a aaa a ", "1 2 2 1"}, + new Object[][] { + {"a", 5}, + {" b", 3}, + }, + new Object[][] { + {"1", 8}, + {" a", 6}, + {" 2", 4}, + {" b", 2} + }); } /* @@ -730,133 +711,126 @@ void testActivationBeforeMethodStarts() throws Exception { */ @Test void testDectivationAfterEnd() throws Exception { - assertCallTree(new String[] { - " dd ", - " c ccc ", - " bb bbb ", // <- deactivation for span 2 happens after b ends - " aaa aaa aa ", // that means b must have ended after 2 has been deactivated - "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 - }, new Object[][] { - {"a", 8}, - {" b", 5}, - {" c", 4}, - {" d", 2}, - }, new Object[][] { - {"1", 11}, - {" a", 9}, - {" b", 6}, - {" c", 5}, - {" 2", 4}, - {" d", 1}, - }); + assertCallTree( + new String[] { + " dd ", + " c ccc ", + " bb bbb ", // <- deactivation for span 2 happens after b ends + " aaa aaa aa ", // that means b must have ended after 2 has been deactivated + "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 + }, + new Object[][] { + {"a", 8}, + {" b", 5}, + {" c", 4}, + {" d", 2}, + }, + new Object[][] { + {"1", 11}, + {" a", 9}, + {" b", 6}, + {" c", 5}, + {" 2", 4}, + {" d", 1}, + }); } @Test void testCallTreeActivationAsParentOfFastSpan() throws Exception { - assertCallTree(new String[] { - " b ", - " aa a aa ", - "1 2 2 1" - }, new Object[][] { - {"a", 5} - }, new Object[][] { - {"1", 8}, - {" a", 6}, - {" 2", 2}, - }); + assertCallTree( + new String[] {" b ", " aa a aa ", "1 2 2 1"}, + new Object[][] {{"a", 5}}, + new Object[][] { + {"1", 8}, + {" a", 6}, + {" 2", 2}, + }); } @Test void testCallTreeActivationAsChildOfFastSpan() throws Exception { profilerSetup.close(); - profilerSetup = ProfilerTestSetup.create( - config -> config - .inferredSpansMinDuration(Duration.ofMillis(50)).clock(nanoClock) - .startScheduledProfiling(false)); + profilerSetup = + ProfilerTestSetup.create( + config -> + config + .inferredSpansMinDuration(Duration.ofMillis(50)) + .clock(nanoClock) + .startScheduledProfiling(false)); profilerSetup.profiler.setProfilingSessionOngoing(true); - assertCallTree(new String[] { - " c c ", - " b b ", - " aaa aaa ", - "1 22 1" - }, new Object[][] { - {"a", 6} - }, new Object[][] { - {"1", 9}, - {" a", 7}, - {" 2", 1}, - }); + assertCallTree( + new String[] {" c c ", " b b ", " aaa aaa ", "1 22 1"}, + new Object[][] {{"a", 6}}, + new Object[][] { + {"1", 9}, + {" a", 7}, + {" 2", 1}, + }); } @Test void testCallTreeActivationAsLeaf() throws Exception { - assertCallTree(new String[] { - " aa aa ", - "1 22 1" - }, new Object[][] { - {"a", 4} - }, new Object[][] { - {"1", 7}, - {" a", 5}, - {" 2", 1}, - }); + assertCallTree( + new String[] {" aa aa ", "1 22 1"}, + new Object[][] {{"a", 4}}, + new Object[][] { + {"1", 7}, + {" a", 5}, + {" 2", 1}, + }); } - @Test void testCallTreeMultipleActivationsAsLeaf() throws Exception { - assertCallTree(new String[] { - " aa aaa aa ", - "1 22 33 1" - }, new Object[][] { - {"a", 7} - }, new Object[][] { - {"1", 12}, - {" a", 10}, - {" 2", 1}, - {" 3", 1}, - }); + assertCallTree( + new String[] {" aa aaa aa ", "1 22 33 1"}, + new Object[][] {{"a", 7}}, + new Object[][] { + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, + }); } @Test void testCallTreeMultipleActivationsAsLeafWithExcludedParent() throws Exception { profilerSetup.close(); - profilerSetup = ProfilerTestSetup.create( - config -> config.clock(nanoClock) - .startScheduledProfiling(false) - .inferredSpansMinDuration(Duration.ofMillis(50))); + profilerSetup = + ProfilerTestSetup.create( + config -> + config + .clock(nanoClock) + .startScheduledProfiling(false) + .inferredSpansMinDuration(Duration.ofMillis(50))); profilerSetup.profiler.setProfilingSessionOngoing(true); // min duration 4 - assertCallTree(new String[] { - " b b c c ", - " aa aaa aa ", - "1 22 33 1" - }, new Object[][] { - {"a", 7} - }, new Object[][] { - {"1", 12}, - {" a", 10}, - {" 2", 1}, - {" 3", 1}, - }); + assertCallTree( + new String[] {" b b c c ", " aa aaa aa ", "1 22 33 1"}, + new Object[][] {{"a", 7}}, + new Object[][] { + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, + }); } @Test void testCallTreeMultipleActivationsWithOneChild() throws Exception { - assertCallTree(new String[] { - " bb ", - " aa aaa aa aa ", - "1 22 3 3 1" - }, new Object[][] { - {"a", 9}, - {" b", 2} - }, new Object[][] { - {"1", 14}, - {" a", 12}, - {" 2", 1}, - {" 3", 3}, - {" b", 1}, - }); + assertCallTree( + new String[] {" bb ", " aa aaa aa aa ", "1 22 3 3 1"}, + new Object[][] { + {"a", 9}, + {" b", 2} + }, + new Object[][] { + {"1", 14}, + {" a", 12}, + {" 2", 1}, + {" 3", 3}, + {" b", 1}, + }); } /* @@ -869,24 +843,25 @@ void testCallTreeMultipleActivationsWithOneChild() throws Exception { @Test @Disabled("fix me") void testNestedActivationBeforeCallTree() throws Exception { - assertCallTree(new String[] { - " aaa ", - "12 2 1" - }, new Object[][] { - {"a", 3}, - }, new Object[][] { - {"1", 5}, - {" a", 3}, // a is actually a child of the transaction - {" 2", 2}, // 2 is not within the child_ids of a - }); + assertCallTree( + new String[] {" aaa ", "12 2 1"}, + new Object[][] { + {"a", 3}, + }, + new Object[][] { + {"1", 5}, + {" a", 3}, // a is actually a child of the transaction + {" 2", 2}, // 2 is not within the child_ids of a + }); } private void assertCallTree(String[] stackTraces, Object[][] expectedTree) throws Exception { assertCallTree(stackTraces, expectedTree, null); } - private Map assertCallTree(String[] stackTraces, Object[][] expectedTree, - @Nullable Object[][] expectedSpans) throws Exception { + private Map assertCallTree( + String[] stackTraces, Object[][] expectedTree, @Nullable Object[][] expectedSpans) + throws Exception { CallTree.Root root = getCallTree(profilerSetup, stackTraces); StringBuilder expectedResult = new StringBuilder(); for (int i = 0; i < expectedTree.length; i++) { @@ -897,24 +872,22 @@ private Map assertCallTree(String[] stackTraces, Object[][] ex } } - String actualResult = root.toString() - .replace(CallTreeTest.class.getName() + ".", ""); - actualResult = Arrays.stream(actualResult.split("\n")) - // skip root node - .skip(1) - // trim first two spaces - .map(s -> s.substring(2)) - .collect(Collectors.joining("\n")); + String actualResult = root.toString().replace(CallTreeTest.class.getName() + ".", ""); + actualResult = + Arrays.stream(actualResult.split("\n")) + // skip root node + .skip(1) + // trim first two spaces + .map(s -> s.substring(2)) + .collect(Collectors.joining("\n")); assertThat(actualResult).isEqualTo(expectedResult.toString()); if (expectedSpans != null) { root.spanify(nanoClock, profilerSetup.sdk.getTracer("dummy-inferred-spans-tracer")); - Map spans = profilerSetup.getSpans() - .stream() - .collect(toMap( - s -> s.getName().replaceAll(".*#", ""), - Function.identity())); + Map spans = + profilerSetup.getSpans().stream() + .collect(toMap(s -> s.getName().replaceAll(".*#", ""), Function.identity())); assertThat(profilerSetup.getSpans()).hasSize(expectedSpans.length + 1); for (int i = 0; i < expectedSpans.length; i++) { @@ -932,25 +905,29 @@ private Map assertCallTree(String[] stackTraces, Object[][] ex assertThat(spans).containsKey(parentName); SpanData span = spans.get(spanName); assertThat(isChild(spans.get(parentName), span)) - .withFailMessage("Expected %s (%s) to be a child of %s (%s) but was %s (%s)", - spanName, span.getSpanContext().getSpanId(), - parentName, spans.get(parentName).getSpanId(), - profilerSetup.getSpans() - .stream() - .filter(s -> s.getSpanId() - .equals(span.getParentSpanId())).findAny() + .withFailMessage( + "Expected %s (%s) to be a child of %s (%s) but was %s (%s)", + spanName, + span.getSpanContext().getSpanId(), + parentName, + spans.get(parentName).getSpanId(), + profilerSetup.getSpans().stream() + .filter(s -> s.getSpanId().equals(span.getParentSpanId())) + .findAny() .map(SpanData::getName) .orElse(null), span.getParentSpanId()) .isTrue(); assertThat(isChild(span, spans.get(parentName))) - .withFailMessage("Expected %s (%s) to not be a child of %s (%s) but was %s (%s)", - parentName, spans.get(parentName).getSpanId(), - spanName, span.getSpanId(), - profilerSetup.getSpans() - .stream() - .filter(s -> s.getSpanId() - .equals(span.getParentSpanId())).findAny() + .withFailMessage( + "Expected %s (%s) to not be a child of %s (%s) but was %s (%s)", + parentName, + spans.get(parentName).getSpanId(), + spanName, + span.getSpanId(), + profilerSetup.getSpans().stream() + .filter(s -> s.getSpanId().equals(span.getParentSpanId())) + .findAny() .map(SpanData::getName) .orElse(null), span.getParentSpanId()) @@ -963,10 +940,16 @@ private Map assertCallTree(String[] stackTraces, Object[][] ex if (stackTrace == null || stackTrace.isEmpty()) { assertThat(actualStacktrace).isBlank(); } else { - String expected = stackTrace.stream() - .map(funcName -> "at " + CallTreeTest.class.getName() + "." + funcName - + "(CallTreeTest.java)") - .collect(Collectors.joining("\n")); + String expected = + stackTrace.stream() + .map( + funcName -> + "at " + + CallTreeTest.class.getName() + + "." + + funcName + + "(CallTreeTest.java)") + .collect(Collectors.joining("\n")); assertThat(actualStacktrace).isEqualTo(expected); } } @@ -986,8 +969,8 @@ public boolean isChild(SpanData parent, SpanData expectedChild) { Boolean isChild = link.getAttributes().get(CallTree.IS_CHILD_ATTRIBUTE_KEY); if (isChild != null && isChild) { SpanContext linkSpanCtx = link.getSpanContext(); - if (linkSpanCtx.getTraceId().equals(expectedChild.getTraceId()) && linkSpanCtx.getSpanId() - .equals(expectedChild.getSpanId())) { + if (linkSpanCtx.getTraceId().equals(expectedChild.getTraceId()) + && linkSpanCtx.getSpanId().equals(expectedChild.getSpanId())) { return true; } } @@ -1029,9 +1012,8 @@ public static CallTree.Root getCallTree(ProfilerTestSetup profilerSetup, String[ Tracer tracer = profilerSetup.sdk.getTracer("testing-tracer"); - Span transaction = tracer.spanBuilder("Call Tree Root") - .setStartTimestamp(1, TimeUnit.NANOSECONDS) - .startSpan(); + Span transaction = + tracer.spanBuilder("Call Tree Root").setStartTimestamp(1, TimeUnit.NANOSECONDS).startSpan(); try (Scope scope = transaction.makeCurrent()) { List stackTraceEvents = new ArrayList<>(); for (int i = 0; i < stackTraces[0].length(); i++) { @@ -1040,8 +1022,8 @@ public static CallTree.Root getCallTree(ProfilerTestSetup profilerSetup, String[ for (String stackTrace : stackTraces) { char c = stackTrace.charAt(i); if (Character.isDigit(c)) { - handleSpanEvent(tracer, spanMap, spanScopeMap, Character.toString(c), - nanoClock.nanoTime()); + handleSpanEvent( + tracer, spanMap, spanScopeMap, Character.toString(c), nanoClock.nanoTime()); break; } else if (!Character.isSpaceChar(c)) { trace.add(StackFrame.of(CallTreeTest.class.getName(), Character.toString(c))); @@ -1061,7 +1043,10 @@ public static CallTree.Root getCallTree(ProfilerTestSetup profilerSetup, String[ assertThat(root).isNotNull(); } long millis = profilerSetup.profiler.config.getInferredSpansMinDuration().toMillis(); - root.addStackTrace(stackTraceEvent.trace, stackTraceEvent.nanoTime, callTreePool, + root.addStackTrace( + stackTraceEvent.trace, + stackTraceEvent.nanoTime, + callTreePool, TimeUnit.MILLISECONDS.toNanos(millis)); } @@ -1086,14 +1071,19 @@ public StackTraceEvent(List trace, long nanoTime) { } } - private static void handleSpanEvent(Tracer tracer, Map spanMap, + private static void handleSpanEvent( + Tracer tracer, + Map spanMap, Map spanScopeMap, - String name, long nanoTime) { + String name, + long nanoTime) { if (!spanMap.containsKey(name)) { - Span span = tracer.spanBuilder(name) - .setParent(Context.current()) - .setStartTimestamp(nanoTime, TimeUnit.NANOSECONDS) - .startSpan(); + Span span = + tracer + .spanBuilder(name) + .setParent(Context.current()) + .setStartTimestamp(nanoTime, TimeUnit.NANOSECONDS) + .startSpan(); spanMap.put(name, span); spanScopeMap.put(name, span.makeCurrent()); } else { @@ -1101,5 +1091,4 @@ private static void handleSpanEvent(Tracer tracer, Map spanMap, spanMap.get(name).end(nanoTime, TimeUnit.NANOSECONDS); } } - } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java index 6b1f59018..608eb033a 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java @@ -18,7 +18,6 @@ */ package co.elastic.apm.otel.profiler; -import co.elastic.apm.otel.profiler.NanoClock; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.trace.ReadWriteSpan; @@ -28,9 +27,7 @@ public class FixedNanoClock implements NanoClock { private long nanoTime = -1L; @Override - public void onSpanStart(ReadWriteSpan started, Context parentContext) { - - } + public void onSpanStart(ReadWriteSpan started, Context parentContext) {} @Override public long nanoTime() { @@ -51,9 +48,7 @@ public long toEpochNanos(long anchor, long recordedNanoTime) { } @Override - public void periodicCleanup() { - - } + public void periodicCleanup() {} public void setNanoTime(long nanoTime) { this.nanoTime = nanoTime; diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java index 694d32c7d..a2d352c20 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler; import io.opentelemetry.sdk.OpenTelemetrySdk; @@ -16,9 +34,8 @@ public class ProfilerTestSetup implements AutoCloseable { InMemorySpanExporter spanExporter; - - public ProfilerTestSetup(OpenTelemetrySdk sdk, InferredSpansProcessor processor, - InMemorySpanExporter spanExporter) { + public ProfilerTestSetup( + OpenTelemetrySdk sdk, InferredSpansProcessor processor, InMemorySpanExporter spanExporter) { this.sdk = sdk; this.profiler = processor.profiler; this.spanExporter = spanExporter; @@ -41,17 +58,15 @@ public static ProfilerTestSetup create(Consumer c InMemorySpanExporter exporter = InMemorySpanExporter.create(); - SdkTracerProvider tracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(processor) - .addSpanProcessor(SimpleSpanProcessor.create(exporter)) - .build(); + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(processor) + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build(); processor.setTracerProvider(tracerProvider); - OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() - .setTracerProvider(tracerProvider) - .build(); + OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build(); return new ProfilerTestSetup(sdk, processor, exporter); } - } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java index dde30545d..32c7045d9 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java @@ -36,18 +36,19 @@ public class SamplingProfilerQueueTest { @DisabledOnAppleSilicon void testFillQueue() throws Exception { - try (ProfilerTestSetup setup = ProfilerTestSetup.create( - config -> config.clock(new FixedNanoClock()).startScheduledProfiling(false))) { + try (ProfilerTestSetup setup = + ProfilerTestSetup.create( + config -> config.clock(new FixedNanoClock()).startScheduledProfiling(false))) { setup.profiler.setProfilingSessionOngoing(true); - Span traceContext = Span.wrap( - SpanContext.create( - "0af7651916cd43dd8448eb211c80319c", - "b7ad6b7169203331", - TraceFlags.getSampled(), - TraceState.getDefault() - )); + Span traceContext = + Span.wrap( + SpanContext.create( + "0af7651916cd43dd8448eb211c80319c", + "b7ad6b7169203331", + TraceFlags.getSampled(), + TraceState.getDefault())); assertThat(setup.profiler.onActivation(traceContext, null)).isTrue(); @@ -63,6 +64,5 @@ void testFillQueue() throws Exception { // now there should be free slots assertThat(setup.profiler.onActivation(traceContext, null)).isTrue(); } - } } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java index 5e43a9ead..e0f73d6d2 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java @@ -28,9 +28,10 @@ import java.util.stream.Collectors; /** - * Can be used in combination with the files created by - * {@link ProfilingConfiguration#backupDiagnosticFiles} to replay the creation of profiler-inferred spans. - * This is useful, for example, to troubleshoot why {@link co.elastic.apm.agent.impl.transaction.Span#childIds} are set as expected. + * Can be used in combination with the files created by {@link + * ProfilingConfiguration#backupDiagnosticFiles} to replay the creation of profiler-inferred spans. + * This is useful, for example, to troubleshoot why {@link + * co.elastic.apm.agent.impl.transaction.Span#childIds} are set as expected. */ public class SamplingProfilerReplay { @@ -43,23 +44,31 @@ public static void main(String[] args) throws Exception { File jfrFile = File.createTempFile("traces", ".jfr"); jfrFile.deleteOnExit(); - try (ProfilerTestSetup setup = ProfilerTestSetup.create( - config -> config - .startScheduledProfiling(false) - .activationEventsFile(activationEventsFile) - .jfrFile(jfrFile) - )) { + try (ProfilerTestSetup setup = + ProfilerTestSetup.create( + config -> + config + .startScheduledProfiling(false) + .activationEventsFile(activationEventsFile) + .jfrFile(jfrFile))) { Path baseDir = Paths.get(System.getProperty("java.io.tmpdir"), "profiler"); - List activationFiles = Files.list(baseDir) - .filter(p -> p.toString().endsWith("activations.dat")).sorted() - .collect(Collectors.toList()); - List traceFiles = Files.list(baseDir).filter(p -> p.toString().endsWith("traces.jfr")) - .sorted().collect(Collectors.toList()); + List activationFiles = + Files.list(baseDir) + .filter(p -> p.toString().endsWith("activations.dat")) + .sorted() + .collect(Collectors.toList()); + List traceFiles = + Files.list(baseDir) + .filter(p -> p.toString().endsWith("traces.jfr")) + .sorted() + .collect(Collectors.toList()); if (traceFiles.size() != activationFiles.size()) { throw new IllegalStateException(); } for (int i = 0; i < activationFiles.size(); i++) { - logger.log(Level.INFO, "processing {0} {1}", + logger.log( + Level.INFO, + "processing {0} {1}", new Object[] {activationFiles.get(i), traceFiles.get(i)}); setup.profiler.copyFromFiles(activationFiles.get(i), traceFiles.get(i)); setup.profiler.processTraces(); diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java index c42f38f38..6aa0e8769 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java @@ -18,7 +18,6 @@ */ package co.elastic.apm.otel.profiler; - import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.atLeast; @@ -97,9 +96,7 @@ void shouldLazilyCreateTempFilesAndCleanThem() throws Exception { awaitProfilerStarted(setup.profiler); - assertThat(getProfilerTempFiles()) - .describedAs("should have created two temp files") - .hasSize(2); + assertThat(getProfilerTempFiles()).describedAs("should have created two temp files").hasSize(2); setup.close(); setup = null; @@ -107,8 +104,6 @@ void shouldLazilyCreateTempFilesAndCleanThem() throws Exception { assertThat(getProfilerTempFiles()) .describedAs("should delete temp files when profiler is stopped") .isEmpty(); - - } private static List getProfilerTempFiles() { @@ -123,16 +118,14 @@ private static List getProfilerTempFiles() { } } - @Test void shouldNotDeleteProvidedFiles() throws Exception { // when an existing file is provided to the profiler, we should not delete it // unlike the temporary files that are created by profiler itself InferredSpansConfiguration defaultConfig; - try (InferredSpansProcessor profiler1 = InferredSpansProcessor.builder() - .startScheduledProfiling(false) - .build()) { + try (InferredSpansProcessor profiler1 = + InferredSpansProcessor.builder().startScheduledProfiling(false).build()) { defaultConfig = profiler1.profiler.config; } @@ -141,11 +134,13 @@ void shouldNotDeleteProvidedFiles() throws Exception { try (OpenTelemetrySdk sdk = OpenTelemetrySdk.builder().build()) { - SamplingProfiler otherProfiler = new SamplingProfiler( - defaultConfig, - new FixedNanoClock(), - () -> sdk.getTracer("my-tracer"), - tempFile1.toFile(), tempFile2.toFile()); + SamplingProfiler otherProfiler = + new SamplingProfiler( + defaultConfig, + new FixedNanoClock(), + () -> sdk.getTracer("my-tracer"), + tempFile1.toFile(), + tempFile2.toFile()); otherProfiler.start(); awaitProfilerStarted(otherProfiler); @@ -159,23 +154,26 @@ void shouldNotDeleteProvidedFiles() throws Exception { @Test void testStartCommand() { setupProfiler(false); - assertThat(setup.profiler.createStartCommand()).isEqualTo( - "start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0"); + assertThat(setup.profiler.createStartCommand()) + .isEqualTo("start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0"); setup.close(); setupProfiler(config -> config.startScheduledProfiling(false).profilerLoggingEnabled(false)); - assertThat(setup.profiler.createStartCommand()).isEqualTo( - "start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0,log=none"); + assertThat(setup.profiler.createStartCommand()) + .isEqualTo( + "start,jfr,event=wall,cstack=n,interval=5ms,filter,file=null,safemode=0,log=none"); setup.close(); - setupProfiler(config -> config - .startScheduledProfiling(false) - .profilerLoggingEnabled(false) - .samplingInterval(Duration.ofMillis(10)) - .asyncProfilerSafeMode(14) - ); - assertThat(setup.profiler.createStartCommand()).isEqualTo( - "start,jfr,event=wall,cstack=n,interval=10ms,filter,file=null,safemode=14,log=none"); + setupProfiler( + config -> + config + .startScheduledProfiling(false) + .profilerLoggingEnabled(false) + .samplingInterval(Duration.ofMillis(10)) + .asyncProfilerSafeMode(14)); + assertThat(setup.profiler.createStartCommand()) + .isEqualTo( + "start,jfr,event=wall,cstack=n,interval=10ms,filter,file=null,safemode=14,log=none"); } @Test @@ -204,46 +202,53 @@ void testProfileTransaction() throws Exception { assertThat(profilingActiveOnThread).isTrue(); - Optional txData = setup.getSpans().stream() - .filter(s -> s.getName().equals("transaction")) - .findAny(); + Optional txData = + setup.getSpans().stream().filter(s -> s.getName().equals("transaction")).findAny(); assertThat(txData).isPresent(); assertThat(txData.get()).hasNoParent(); - Optional testProfileTransaction = setup.getSpans().stream() - .filter(s -> s.getName().equals("SamplingProfilerTest#testProfileTransaction")) - .findAny(); + Optional testProfileTransaction = + setup.getSpans().stream() + .filter(s -> s.getName().equals("SamplingProfilerTest#testProfileTransaction")) + .findAny(); assertThat(testProfileTransaction).isPresent(); assertThat(testProfileTransaction.get()).hasParent(txData.get()); - Optional inferredSpanA = setup.getSpans().stream() - .filter(s -> s.getName().equals("SamplingProfilerTest#aInferred")).findAny(); + Optional inferredSpanA = + setup.getSpans().stream() + .filter(s -> s.getName().equals("SamplingProfilerTest#aInferred")) + .findAny(); assertThat(inferredSpanA).isPresent(); assertThat(inferredSpanA.get()).hasParent(testProfileTransaction.get()); - Optional explicitSpanB = setup.getSpans().stream() - .filter(s -> s.getName().equals("bExplicit")).findAny(); + Optional explicitSpanB = + setup.getSpans().stream().filter(s -> s.getName().equals("bExplicit")).findAny(); assertThat(explicitSpanB).isPresent(); assertThat(explicitSpanB.get()).hasParent(txData.get()); assertThat(inferredSpanA.get().getLinks()) .hasSize(1) - .anySatisfy(link -> { - assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); - SpanData expectedSpan = explicitSpanB.get(); - Assertions.assertThat(link.getSpanContext().getTraceId()) - .isEqualTo(expectedSpan.getTraceId()); - Assertions.assertThat(link.getSpanContext().getSpanId()) - .isEqualTo(expectedSpan.getSpanId()); - }); - - Optional inferredSpanC = setup.getSpans().stream() - .filter(s -> s.getName().equals("SamplingProfilerTest#cInferred")).findAny(); + .anySatisfy( + link -> { + assertThat(link.getAttributes()).containsEntry("elastic.is_child", true); + SpanData expectedSpan = explicitSpanB.get(); + Assertions.assertThat(link.getSpanContext().getTraceId()) + .isEqualTo(expectedSpan.getTraceId()); + Assertions.assertThat(link.getSpanContext().getSpanId()) + .isEqualTo(expectedSpan.getSpanId()); + }); + + Optional inferredSpanC = + setup.getSpans().stream() + .filter(s -> s.getName().equals("SamplingProfilerTest#cInferred")) + .findAny(); assertThat(inferredSpanC).isPresent(); assertThat(inferredSpanC.get()).hasParent(explicitSpanB.get()); - Optional inferredSpanD = setup.getSpans().stream() - .filter(s -> s.getName().equals("SamplingProfilerTest#dInferred")).findAny(); + Optional inferredSpanD = + setup.getSpans().stream() + .filter(s -> s.getName().equals("SamplingProfilerTest#dInferred")) + .findAny(); assertThat(inferredSpanD).isPresent(); assertThat(inferredSpanD.get()).hasParent(inferredSpanC.get()); } @@ -267,14 +272,15 @@ void testVirtualThreadsExcluded() throws Exception { Tracer tracer = setup.sdk.getTracer("manual-spans"); AtomicReference profilingActive = new AtomicReference<>(); - Runnable task = () -> { - Span tx = tracer.spanBuilder("transaction").startSpan(); - try (Scope scope = tx.makeCurrent()) { - profilingActive.set(setup.profiler.isProfilingActiveOnThread(Thread.currentThread())); - } finally { - tx.end(); - } - }; + Runnable task = + () -> { + Span tx = tracer.spanBuilder("transaction").startSpan(); + try (Scope scope = tx.makeCurrent()) { + profilingActive.set(setup.profiler.isProfilingActiveOnThread(Thread.currentThread())); + } finally { + tx.end(); + } + }; Method startVirtualThread = Thread.class.getMethod("startVirtualThread", Runnable.class); Thread virtual = (Thread) startVirtualThread.invoke(null, task); @@ -304,11 +310,10 @@ void testPostProcessingDisabled() throws Exception { .timeout(5000, TimeUnit.MILLISECONDS) .untilAsserted(() -> assertThat(setup.getSpans()).hasSize(2)); - Optional explicitSpanB = setup.getSpans().stream() - .filter(s -> s.getName().equals("bExplicit")).findAny(); + Optional explicitSpanB = + setup.getSpans().stream().filter(s -> s.getName().equals("bExplicit")).findAny(); assertThat(explicitSpanB).isPresent(); - assertThat(explicitSpanB.get()) - .hasParentSpanId(tx.getSpanContext().getSpanId()); + assertThat(explicitSpanB.get()).hasParentSpanId(tx.getSpanContext().getSpanId()); } private void aInferred(Tracer tracer) throws Exception { @@ -334,17 +339,18 @@ private void setupProfiler(boolean enabled) { setupProfiler(config -> config.startScheduledProfiling(enabled)); } - private void setupProfiler(Consumer configCustomizer) { - setup = ProfilerTestSetup.create(config -> { - config.profilingDuration(Duration.ofMillis(500)) - .profilerInterval(Duration.ofMillis(500)) - .samplingInterval(Duration.ofMillis(5)); - configCustomizer.accept(config); - }); + setup = + ProfilerTestSetup.create( + config -> { + config + .profilingDuration(Duration.ofMillis(500)) + .profilerInterval(Duration.ofMillis(500)) + .samplingInterval(Duration.ofMillis(5)); + configCustomizer.accept(config); + }); } - private static void awaitProfilerStarted(SamplingProfiler profiler) { // ensure profiler is initialized await() diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java index b55722318..8cbc77266 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java @@ -20,7 +20,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import co.elastic.apm.otel.profiler.ThreadMatcher; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; @@ -32,17 +31,21 @@ class ThreadMatcherTest { @Test void testLookup() { ArrayList threads = new ArrayList<>(); - threadMatcher.forEachThread(new ThreadMatcher.NonCapturingPredicate() { - @Override - public boolean test(Thread thread, Void state) { - return thread.getId() == Thread.currentThread().getId(); - } - }, null, new ThreadMatcher.NonCapturingConsumer>() { - @Override - public void accept(Thread thread, List state) { - state.add(thread); - } - }, threads); + threadMatcher.forEachThread( + new ThreadMatcher.NonCapturingPredicate() { + @Override + public boolean test(Thread thread, Void state) { + return thread.getId() == Thread.currentThread().getId(); + } + }, + null, + new ThreadMatcher.NonCapturingConsumer>() { + @Override + public void accept(Thread thread, List state) { + state.add(thread); + } + }, + threads); assertThat(threads).isEqualTo(List.of(Thread.currentThread())); } } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java index ba36fc5d9..96c07e3f6 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java @@ -55,8 +55,8 @@ void testShouldCopyLibToSpecifiedDirectory(@TempDir File nonDefaultTempDirectory AsyncProfiler.getInstance(nonDefaultTempDirectory.getAbsolutePath(), 6); assertThat(Integer.valueOf(System.getProperty(SAFEMODE_SYSTEM_PROPERTY_NAME))).isEqualTo(6); - File[] libasyncProfilers = nonDefaultTempDirectory.listFiles( - getLibasyncProfilerFilenameFilter()); + File[] libasyncProfilers = + nonDefaultTempDirectory.listFiles(getLibasyncProfilerFilenameFilter()); assertThat(libasyncProfilers).hasSizeGreaterThanOrEqualTo(1); } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java index d94b53696..988ffe454 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java @@ -46,8 +46,8 @@ import org.kohsuke.github.PagedIterable; /** - * This test class is disabled by default. It is used as a utility for manually upgrading async profiler - update the - * {@link #TARGET_VERSION} and run on a POSIX-compatible file system. + * This test class is disabled by default. It is used as a utility for manually upgrading async + * profiler - update the {@link #TARGET_VERSION} and run on a POSIX-compatible file system. */ @Disabled public class AsyncProfilerUpgrader { @@ -57,11 +57,7 @@ public class AsyncProfilerUpgrader { static final String COMMON_BINARY_FILE_NAME = "libasyncProfiler.so"; static final String[] USED_ARTIFACTS = { - "linux-aarch64", - "linux-arm", - "linux-x64", - "linux-x86", - "macos-x64" + "linux-aarch64", "linux-arm", "linux-x64", "linux-x86", "macos-x64" }; @Test @@ -70,24 +66,31 @@ void updateAsyncProfilerBinaries() throws Exception { GHRepository repository = github.getRepository("jvm-profiling-tools/async-profiler"); GHRelease release = repository.getReleaseByTagName("v" + TARGET_VERSION); PagedIterable releaseAssets = release.listAssets(); - Path downloadDirPath = Files.createTempDirectory( - String.format("AsyncProfiler_%s_", TARGET_VERSION)); + Path downloadDirPath = + Files.createTempDirectory(String.format("AsyncProfiler_%s_", TARGET_VERSION)); for (GHAsset releaseAsset : releaseAssets) { if (releaseAsset.getContentType().equals("application/x-gzip")) { - downloadAndReplaceBinary(releaseAsset.getBrowserDownloadUrl(), releaseAsset.getName(), - downloadDirPath, releaseAsset.getSize()); + downloadAndReplaceBinary( + releaseAsset.getBrowserDownloadUrl(), + releaseAsset.getName(), + downloadDirPath, + releaseAsset.getSize()); } } // test we are now using the right version - Path thisOsLib = getBinariesResourceDir().resolve( - co.elastic.apm.otel.profiler.asyncprofiler.AsyncProfiler.getLibraryFileName() + ".so"); + Path thisOsLib = + getBinariesResourceDir() + .resolve( + co.elastic.apm.otel.profiler.asyncprofiler.AsyncProfiler.getLibraryFileName() + + ".so"); AsyncProfiler asyncProfiler = AsyncProfiler.getInstance(thisOsLib.toString()); assertThat(asyncProfiler.getVersion()).isEqualTo(TARGET_VERSION); } - private void downloadAndReplaceBinary(String ghAssetDownloadUrl, String ghAssetName, - Path targetDownloadDir, long expectedSize) throws Exception { + private void downloadAndReplaceBinary( + String ghAssetDownloadUrl, String ghAssetName, Path targetDownloadDir, long expectedSize) + throws Exception { String artifactNamePattern = null; for (String artifact : USED_ARTIFACTS) { if (ghAssetName.contains(artifact)) { @@ -102,10 +105,10 @@ private void downloadAndReplaceBinary(String ghAssetDownloadUrl, String ghAssetN } Path targetDownloadPath = targetDownloadDir.resolve(ghAssetName); System.out.println( - String.format("Downloading from %s into %s and extracting binary", ghAssetName, - targetDownloadDir)); - Path localBinaryPath = downloadAndExtractBinary(ghAssetDownloadUrl, targetDownloadPath, - expectedSize); + String.format( + "Downloading from %s into %s and extracting binary", ghAssetName, targetDownloadDir)); + Path localBinaryPath = + downloadAndExtractBinary(ghAssetDownloadUrl, targetDownloadPath, expectedSize); assertThat(localBinaryPath) .describedAs("Failed to download and extract binary file from " + ghAssetDownloadUrl) .isNotNull(); @@ -115,8 +118,8 @@ private void downloadAndReplaceBinary(String ghAssetDownloadUrl, String ghAssetN } @Nullable - private Path downloadAndExtractBinary(String ghAssetDownloadUrl, Path targetDownloadPath, - long expectedSize) throws IOException { + private Path downloadAndExtractBinary( + String ghAssetDownloadUrl, Path targetDownloadPath, long expectedSize) throws IOException { System.out.println("Downloading from " + ghAssetDownloadUrl); URLConnection assetUrlConnection = new URL(ghAssetDownloadUrl).openConnection(); long actualSize; @@ -132,16 +135,17 @@ private Path extractBinaryFileFromArchive(Path assetArchivePath) throws IOExcept String archiveFileName = assetArchivePath.getFileName().toString(); if (!archiveFileName.endsWith(TAR_GZ_FILE_EXTENSION)) { throw new IllegalArgumentException( - String.format("Cannot extract %s - expecting a path to a %s file", archiveFileName, - TAR_GZ_FILE_EXTENSION)); + String.format( + "Cannot extract %s - expecting a path to a %s file", + archiveFileName, TAR_GZ_FILE_EXTENSION)); } Path assetDirPath = assetArchivePath.getParent(); if (!Files.exists(assetDirPath)) { Files.createDirectory(assetDirPath); } - String extractedDirName = archiveFileName.substring(0, - archiveFileName.length() - TAR_GZ_FILE_EXTENSION.length()); + String extractedDirName = + archiveFileName.substring(0, archiveFileName.length() - TAR_GZ_FILE_EXTENSION.length()); Path extractedDirPath = assetDirPath.resolve(extractedDirName); if (!Files.exists(extractedDirPath)) { Files.createDirectory(extractedDirPath); @@ -169,22 +173,23 @@ private Path extractBinaryFileFromArchive(Path assetArchivePath) throws IOExcept /** * Replaces an existing binary file with its downloaded counterpart. - *

    - * NOTE: when replacing the existing binary file, this method attempts to apply the current binary file's - * permissions to the one replacing it, assuming the underlying file system is POSIX-compatible. If this is - * not the case, and error will occur - *

    + * + *

    NOTE: when replacing the existing binary file, this method attempts to apply the current + * binary file's permissions to the one replacing it, assuming the underlying file system is + * POSIX-compatible. If this is not the case, and error will occur * * @param downloadedArtifact the path to the downloaded binary file * @param artifactName the name of the artifact to replace, see {@link #USED_ARTIFACTS} - * @throws Exception thrown when an error occurs while trying to replace, or when running on non POSIX file system + * @throws Exception thrown when an error occurs while trying to replace, or when running on non + * POSIX file system */ private void replaceBinary(Path downloadedArtifact, String artifactName) throws Exception { if (!downloadedArtifact.toString().contains(artifactName)) { throw new IllegalArgumentException( - String.format("the provided path for the downloaded artifact [%s] must " + - "be of a file containing the provided artifact name: %s", downloadedArtifact, - artifactName)); + String.format( + "the provided path for the downloaded artifact [%s] must " + + "be of a file containing the provided artifact name: %s", + downloadedArtifact, artifactName)); } Path binariesResourceDir = getBinariesResourceDir(); @@ -196,20 +201,24 @@ private void replaceBinary(Path downloadedArtifact, String artifactName) throws } System.out.println( String.format("Replacing %s with %s", binaryResourcePath, downloadedArtifact)); - Set posixFilePermissions = Files.getPosixFilePermissions( - binaryResourcePath); + Set posixFilePermissions = + Files.getPosixFilePermissions(binaryResourcePath); Files.move(downloadedArtifact, binaryResourcePath, StandardCopyOption.REPLACE_EXISTING); Files.setPosixFilePermissions(binaryResourcePath, posixFilePermissions); } private Path getBinariesResourceDir() throws URISyntaxException { // /apm-agent-plugins/apm-profiling-plugin/target/classes/asyncprofiler - Path asyncProfilerTestResourcePath = Paths.get( - AsyncProfilerUpgrader.class.getResource("/asyncprofiler").toURI()); + Path asyncProfilerTestResourcePath = + Paths.get(AsyncProfilerUpgrader.class.getResource("/asyncprofiler").toURI()); // /apm-agent-plugins/apm-profiling-plugin/target/classes/asyncprofiler Path pluginRootDir = asyncProfilerTestResourcePath.getParent().getParent().getParent(); - // We are looking for // /apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler - return pluginRootDir.resolve("src").resolve("main").resolve("resources") + // We are looking for // + // /apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler + return pluginRootDir + .resolve("src") + .resolve("main") + .resolve("resources") .resolve("asyncprofiler"); } } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java index d1b4c8d55..24f60c223 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java @@ -40,23 +40,24 @@ void name() throws Exception { // should trigger most edge cases in the buffer being exhausted JfrParser jfrParser = new JfrParser(ByteBuffer.allocate(113), ByteBuffer.allocate(113)); - File file = Paths.get(JfrParserTest.class.getClassLoader().getResource("recording.jfr").toURI()) - .toFile(); + File file = + Paths.get(JfrParserTest.class.getClassLoader().getResource("recording.jfr").toURI()) + .toFile(); jfrParser.parse(file, List.of(), List.of(caseSensitiveMatcher("co.elastic.apm.*"))); AtomicInteger stackTraces = new AtomicInteger(); ArrayList stackFrames = new ArrayList<>(); - jfrParser.consumeStackTraces((threadId, stackTraceId, nanoTime) -> { - jfrParser.resolveStackTrace(stackTraceId, true, stackFrames, MAX_STACK_DEPTH); - if (!stackFrames.isEmpty()) { - stackTraces.incrementAndGet(); - assertThat(stackFrames.get(stackFrames.size() - 1).getMethodName()).isEqualTo( - "testProfileTransaction"); - assertThat(stackFrames).hasSizeLessThanOrEqualTo(MAX_STACK_DEPTH); - } - stackFrames.clear(); - }); + jfrParser.consumeStackTraces( + (threadId, stackTraceId, nanoTime) -> { + jfrParser.resolveStackTrace(stackTraceId, true, stackFrames, MAX_STACK_DEPTH); + if (!stackFrames.isEmpty()) { + stackTraces.incrementAndGet(); + assertThat(stackFrames.get(stackFrames.size() - 1).getMethodName()) + .isEqualTo("testProfileTransaction"); + assertThat(stackFrames).hasSizeLessThanOrEqualTo(MAX_STACK_DEPTH); + } + stackFrames.clear(); + }); assertThat(stackTraces.get()).isEqualTo(97); } - } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java index c6dc78405..c28210e74 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.util; import java.lang.annotation.Documented; @@ -13,8 +31,6 @@ @ExtendWith(DisabledOnAppleSiliconCondition.class) public @interface DisabledOnAppleSilicon { - /** - * The reason this annotated test class or test method is disabled. - */ + /** The reason this annotated test class or test method is disabled. */ String value() default ""; } diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java index 44d4079c8..1381b71f9 100644 --- a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java +++ b/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler.util; import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; @@ -15,9 +33,11 @@ public class DisabledOnAppleSiliconCondition implements ExecutionCondition { public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { AnnotatedElement element = context.getElement().orElse(null); return findAnnotation(element, DisabledOnAppleSilicon.class) - .map(annotation -> isOnAppleSilicon() - ? disabled(element + " is @DisabledOnAppleSilicon", annotation.value()) - : enabled("Not running on Apple silicon")) + .map( + annotation -> + isOnAppleSilicon() + ? disabled(element + " is @DisabledOnAppleSilicon", annotation.value()) + : enabled("Not running on Apple silicon")) .orElse(enabled("@DisabledOnAppleSilicon is not present")); } From 55e6ac7a9db9b9e89da7f7b269a57d12ccd22cb7 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Thu, 23 Nov 2023 12:46:06 +0100 Subject: [PATCH 05/18] Renamed project from "inferred-spans" to "inferred-spans-otel" --- .../build.gradle.kts | 0 .../java/co/elastic/apm/otel/profiler/CallTree.java | 0 .../otel/profiler/InferredSpansConfiguration.java | 0 .../apm/otel/profiler/InferredSpansProcessor.java | 0 .../profiler/InferredSpansProcessorBuilder.java | 0 .../co/elastic/apm/otel/profiler/NanoClock.java | 0 .../otel/profiler/ProfilingActivationListener.java | 0 .../elastic/apm/otel/profiler/SamplingProfiler.java | 0 .../apm/otel/profiler/SpanAnchoredNanoClock.java | 0 .../co/elastic/apm/otel/profiler/StackFrame.java | 0 .../co/elastic/apm/otel/profiler/ThreadMatcher.java | 0 .../co/elastic/apm/otel/profiler/TraceContext.java | 0 .../otel/profiler/asyncprofiler/AsyncProfiler.java | 0 .../otel/profiler/asyncprofiler/BufferedFile.java | 0 .../apm/otel/profiler/asyncprofiler/JfrParser.java | 0 .../asyncprofiler/ResourceExtractionUtil.java | 0 .../otel/profiler/collections/CollectionUtil.java | 0 .../apm/otel/profiler/collections/Hashing.java | 0 .../otel/profiler/collections/Int2IntHashMap.java | 0 .../profiler/collections/Int2ObjectHashMap.java | 0 .../otel/profiler/collections/IntIntConsumer.java | 0 .../otel/profiler/collections/Long2LongHashMap.java | 0 .../profiler/collections/Long2ObjectHashMap.java | 0 .../apm/otel/profiler/collections/LongHashSet.java | 0 .../apm/otel/profiler/collections/LongList.java | 0 .../otel/profiler/collections/LongLongConsumer.java | 0 .../apm/otel/profiler/collections/package-info.java | 0 .../apm/otel/profiler/config/WildcardMatcher.java | 0 .../otel/profiler/pooling/AbstractObjectPool.java | 0 .../apm/otel/profiler/pooling/Allocator.java | 0 .../apm/otel/profiler/pooling/ObjectPool.java | 0 .../otel/profiler/pooling/QueueBasedObjectPool.java | 0 .../apm/otel/profiler/pooling/Recyclable.java | 0 .../elastic/apm/otel/profiler/pooling/Resetter.java | 0 .../elastic/apm/otel/profiler/util/ByteUtils.java | 0 .../co/elastic/apm/otel/profiler/util/HexUtils.java | 0 .../elastic/apm/otel/profiler/util/ThreadUtils.java | 0 .../asyncprofiler/libasyncProfiler-linux-aarch64.so | Bin .../asyncprofiler/libasyncProfiler-linux-arm.so | Bin .../asyncprofiler/libasyncProfiler-linux-x64.so | Bin .../asyncprofiler/libasyncProfiler-linux-x86.so | Bin .../asyncprofiler/libasyncProfiler-macos-x64.so | Bin .../apm/otel/profiler/CallTreeSpanifyTest.java | 0 .../co/elastic/apm/otel/profiler/CallTreeTest.java | 0 .../elastic/apm/otel/profiler/FixedNanoClock.java | 0 .../apm/otel/profiler/ProfilerTestSetup.java | 0 .../otel/profiler/SamplingProfilerQueueTest.java | 0 .../apm/otel/profiler/SamplingProfilerReplay.java | 0 .../apm/otel/profiler/SamplingProfilerTest.java | 0 .../apm/otel/profiler/ThreadMatcherTest.java | 0 .../profiler/asyncprofiler/AsyncProfilerTest.java | 0 .../asyncprofiler/AsyncProfilerUpgrader.java | 0 .../otel/profiler/asyncprofiler/JfrParserTest.java | 0 .../otel/profiler/util/DisabledOnAppleSilicon.java | 0 .../util/DisabledOnAppleSiliconCondition.java | 0 .../src/test/resources/logging.properties | 0 .../src/test/resources/recording.jfr | Bin settings.gradle.kts | 2 +- 58 files changed, 1 insertion(+), 1 deletion(-) rename {inferred-spans => inferred-spans-otel}/build.gradle.kts (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/CallTree.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java (100%) rename {inferred-spans => inferred-spans-otel}/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so (100%) rename {inferred-spans => inferred-spans-otel}/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so (100%) rename {inferred-spans => inferred-spans-otel}/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so (100%) rename {inferred-spans => inferred-spans-otel}/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so (100%) rename {inferred-spans => inferred-spans-otel}/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java (100%) rename {inferred-spans => inferred-spans-otel}/src/test/resources/logging.properties (100%) rename {inferred-spans => inferred-spans-otel}/src/test/resources/recording.jfr (100%) diff --git a/inferred-spans/build.gradle.kts b/inferred-spans-otel/build.gradle.kts similarity index 100% rename from inferred-spans/build.gradle.kts rename to inferred-spans-otel/build.gradle.kts diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/CallTree.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java diff --git a/inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java similarity index 100% rename from inferred-spans/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java diff --git a/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so b/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so similarity index 100% rename from inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so rename to inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so diff --git a/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so b/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so similarity index 100% rename from inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so rename to inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so diff --git a/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so b/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so similarity index 100% rename from inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so rename to inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so diff --git a/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so b/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so similarity index 100% rename from inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so rename to inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so diff --git a/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so b/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so similarity index 100% rename from inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so rename to inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java diff --git a/inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java similarity index 100% rename from inferred-spans/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java diff --git a/inferred-spans/src/test/resources/logging.properties b/inferred-spans-otel/src/test/resources/logging.properties similarity index 100% rename from inferred-spans/src/test/resources/logging.properties rename to inferred-spans-otel/src/test/resources/logging.properties diff --git a/inferred-spans/src/test/resources/recording.jfr b/inferred-spans-otel/src/test/resources/recording.jfr similarity index 100% rename from inferred-spans/src/test/resources/recording.jfr rename to inferred-spans-otel/src/test/resources/recording.jfr diff --git a/settings.gradle.kts b/settings.gradle.kts index 7496cf250..ead16745e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,7 +14,7 @@ include("agent") include("bootstrap") include("custom") include("instrumentation") -include("inferred-spans") +include("inferred-spans-otel") include("resources") include("resources:repackaged") include("smoke-tests") From c155b2f7df3c412322f82983af0dd55624b89ecb Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:26:17 +0100 Subject: [PATCH 06/18] add and configure second copyright header --- build.gradle | 9 ++++++++- buildscripts/spotless.reallogic.license.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 buildscripts/spotless.reallogic.license.java diff --git a/build.gradle b/build.gradle index 45e06c916..afdefdbd1 100644 --- a/build.gradle +++ b/build.gradle @@ -35,9 +35,16 @@ subprojects { spotless { java { + target("src/**/*.java") googleJavaFormat() + licenseHeaderFile(rootProject.file("buildscripts/spotless.license.java"), "(package|import|public)") - target("src/**/*.java") + .named("default") + + licenseHeaderFile(rootProject.file("buildscripts/spotless.reallogic.license.java"), "(package|import|public)") + .named("reallogic") + .onlyIfContentMatches("package co.elastic.apm.otel.profiler.collections;") + } } diff --git a/buildscripts/spotless.reallogic.license.java b/buildscripts/spotless.reallogic.license.java new file mode 100644 index 000000000..77a05d4c4 --- /dev/null +++ b/buildscripts/spotless.reallogic.license.java @@ -0,0 +1,15 @@ +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * + * https://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. + */ From b532d4157e925fbdfd15d38a78498369f6cf8d7e Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:27:05 +0100 Subject: [PATCH 07/18] apply the new copyright header --- .../profiler/collections/CollectionUtil.java | 23 ++++++++----------- .../otel/profiler/collections/Hashing.java | 23 ++++++++----------- .../profiler/collections/Int2IntHashMap.java | 23 ++++++++----------- .../collections/Int2ObjectHashMap.java | 23 ++++++++----------- .../profiler/collections/IntIntConsumer.java | 23 ++++++++----------- .../collections/Long2LongHashMap.java | 23 ++++++++----------- .../collections/Long2ObjectHashMap.java | 23 ++++++++----------- .../profiler/collections/LongHashSet.java | 23 ++++++++----------- .../otel/profiler/collections/LongList.java | 23 ++++++++----------- .../collections/LongLongConsumer.java | 23 ++++++++----------- 10 files changed, 100 insertions(+), 130 deletions(-) diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java index e8a90ee21..f6a56bbc4 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java index 2c1589501..b625b3406 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java index 1935fcd11..c4bd77977 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java index d0f28bf3c..5e914ced1 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java index 748ba3b2b..cc36e085e 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java index 091776144..d29deca46 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java index b5cf3e722..22db251f4 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java index f8c5154de..fd99ce7a7 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java index 5ac25a9a1..1e73ca799 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java index c30bdd85c..d970c772a 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java @@ -1,20 +1,17 @@ /* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. + * Copyright 2014-2019 Real Logic Ltd. + * + * 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 + * https://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. + * 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 co.elastic.apm.otel.profiler.collections; From 280fbcf114de08c62a575f93b9c9095eee5be1ad Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:18:25 +0100 Subject: [PATCH 08/18] restore the double-header --- buildscripts/spotless.reallogic.license.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/buildscripts/spotless.reallogic.license.java b/buildscripts/spotless.reallogic.license.java index 77a05d4c4..6ef2949a3 100644 --- a/buildscripts/spotless.reallogic.license.java +++ b/buildscripts/spotless.reallogic.license.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * From 15dc1676c901b3b090a9cf0b15bcdfd188ebb836 Mon Sep 17 00:00:00 2001 From: Sylvain Juge <763082+SylvainJuge@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:18:53 +0100 Subject: [PATCH 09/18] apply the double header --- .../profiler/collections/CollectionUtil.java | 18 ++++++++++++++++++ .../apm/otel/profiler/collections/Hashing.java | 18 ++++++++++++++++++ .../profiler/collections/Int2IntHashMap.java | 18 ++++++++++++++++++ .../collections/Int2ObjectHashMap.java | 18 ++++++++++++++++++ .../profiler/collections/IntIntConsumer.java | 18 ++++++++++++++++++ .../profiler/collections/Long2LongHashMap.java | 18 ++++++++++++++++++ .../collections/Long2ObjectHashMap.java | 18 ++++++++++++++++++ .../otel/profiler/collections/LongHashSet.java | 18 ++++++++++++++++++ .../otel/profiler/collections/LongList.java | 18 ++++++++++++++++++ .../profiler/collections/LongLongConsumer.java | 18 ++++++++++++++++++ 10 files changed, 180 insertions(+) diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java index f6a56bbc4..2464a5ff6 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java index b625b3406..a8343a5ac 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java index c4bd77977..387121d1b 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java index 5e914ced1..db5361883 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java index cc36e085e..2683217b5 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java index d29deca46..a3a083a1b 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java index 22db251f4..e7f6de552 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java index fd99ce7a7..55850370e 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java index 1e73ca799..7f90a649f 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java index d970c772a..66c85c850 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java @@ -1,3 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ /* * Copyright 2014-2019 Real Logic Ltd. * From e432c3413155e40883cf3764dcfa4e1ef3cf0753 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Wed, 6 Dec 2023 13:58:24 +0100 Subject: [PATCH 10/18] Review fixes --- .../elastic/apm/otel/profiler/CallTree.java | 6 +- .../otel/profiler/InferredSpansProcessor.java | 3 +- .../InferredSpansProcessorBuilder.java | 4 +- .../elastic/apm/otel/profiler/NanoClock.java | 36 -- .../profiler/ProfilingActivationListener.java | 9 +- .../apm/otel/profiler/SamplingProfiler.java | 27 +- ...dNanoClock.java => SpanAnchoredClock.java} | 16 +- .../elastic/apm/otel/profiler/StackFrame.java | 5 +- .../apm/otel/profiler/TraceContext.java | 2 +- .../otel/profiler/CallTreeSpanifyTest.java | 4 +- .../apm/otel/profiler/CallTreeTest.java | 398 +++++++++--------- .../{FixedNanoClock.java => FixedClock.java} | 2 +- .../profiler/SamplingProfilerQueueTest.java | 2 +- .../otel/profiler/SamplingProfilerTest.java | 4 +- 14 files changed, 249 insertions(+), 269 deletions(-) delete mode 100644 inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java rename inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/{SpanAnchoredNanoClock.java => SpanAnchoredClock.java} (79%) rename inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/{FixedNanoClock.java => FixedClock.java} (96%) diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java index 5c6391f0c..9144358b7 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java @@ -446,7 +446,7 @@ int spanify( CallTree.Root root, @Nullable Span parentSpan, TraceContext parentContext, - NanoClock clock, + SpanAnchoredClock clock, StringBuilder tempBuilder, Tracer tracer) { int createdSpans = 0; @@ -481,7 +481,7 @@ protected Span asSpan( @Nullable Span parentSpan, TraceContext parentContext, Tracer tracer, - NanoClock clock, + SpanAnchoredClock clock, StringBuilder tempBuilder) { Context parentOtelCtx; @@ -868,7 +868,7 @@ private CallTree findCommonAncestor(CallTree previousTopOfStack, CallTree topOfS * possible to update the {@link TraceContext#parentId} of a regular span so that it correctly * reflects being a child of an inferred span. */ - public int spanify(NanoClock clock, Tracer tracer) { + public int spanify(SpanAnchoredClock clock, Tracer tracer) { StringBuilder tempBuilder = new StringBuilder(); int createdSpans = 0; List callTrees = getChildren(); diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java index f438c02ea..3f6b17415 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java @@ -45,7 +45,7 @@ public class InferredSpansProcessor implements SpanProcessor { InferredSpansProcessor( InferredSpansConfiguration config, - NanoClock clock, + SpanAnchoredClock clock, boolean startScheduledProfiling, @Nullable File activationEventsFile, @Nullable File jfrFile) { @@ -89,6 +89,7 @@ public boolean isEndRequired() { public CompletableResultCode shutdown() { CompletableResultCode result = new CompletableResultCode(); logger.fine("Stopping Inferred Spans Processor"); + //TODO: Replace with co.elastic.otel.util.ExecutorUtils Executors.newSingleThreadExecutor() .submit( () -> { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java index 5081ff39d..f19e82624 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java @@ -53,7 +53,7 @@ public class InferredSpansProcessorBuilder { private String profilerLibDirectory = null; // The following options are only intended to be modified in tests - private NanoClock clock = new SpanAnchoredNanoClock(); + private SpanAnchoredClock clock = new SpanAnchoredClock(); private boolean startScheduledProfiling = true; private @Nullable File activationEventsFile = null; private @Nullable File jfrFile = null; @@ -175,7 +175,7 @@ public InferredSpansProcessorBuilder profilerLibDirectory(String profilerLibDire } /** For testing only. */ - InferredSpansProcessorBuilder clock(NanoClock clock) { + InferredSpansProcessorBuilder clock(SpanAnchoredClock clock) { this.clock = clock; return this; } diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java deleted file mode 100644 index f8d3c552f..000000000 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/NanoClock.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.apm.otel.profiler; - -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.context.Context; -import io.opentelemetry.sdk.trace.ReadWriteSpan; - -public interface NanoClock { - - void onSpanStart(ReadWriteSpan started, Context parentContext); - - long nanoTime(); - - long getAnchor(Span parent); - - long toEpochNanos(long anchor, long recordedNanoTime); - - void periodicCleanup(); -} diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java index 6f2603b81..fdf876931 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java @@ -32,10 +32,17 @@ public class ProfilingActivationListener implements Closeable { static { + // ContextStorage.addWrapper must + // * happen before anyone accesses any Context + // * happen exactly once + // The "exactly" once part is why we use a static initializer: + // If an Otel-SDK is created and immediately shutdown again and if we create another SDK afterwards, + // we might accidentally register the wrapper twice ContextStorage.addWrapper(ContextStorageWrapper::new); } - public static void ensureInitialized() { + //For testing only + static void ensureInitialized() { // does nothing but ensures that the static initializer ran } diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java index b11a95040..51fc68c2c 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java @@ -128,8 +128,8 @@ public void translateTo( active, Thread.currentThread().getId(), previouslyActive, - nanoClock.nanoTime(), - nanoClock); + clock.nanoTime(), + clock); } }; private final EventTranslatorTwoArg DEACTIVATION_EVENT_TRANSLATOR = @@ -141,8 +141,8 @@ public void translateTo( active, Thread.currentThread().getId(), previouslyActive, - nanoClock.nanoTime(), - nanoClock); + clock.nanoTime(), + clock); } }; // sizeof(ActivationEvent) is 176B so the ring buffer should be around 880KiB @@ -155,7 +155,7 @@ public void translateTo( private final RingBuffer eventBuffer; private volatile boolean profilingSessionOngoing = false; private final Sequence sequence; - private final NanoClock nanoClock; + private final SpanAnchoredClock clock; private final ObjectPool rootPool; private final ThreadMatcher threadMatcher = new ThreadMatcher(); private final EventPoller poller; @@ -198,12 +198,13 @@ public void translateTo( */ SamplingProfiler( InferredSpansConfiguration config, - NanoClock nanoClock, + SpanAnchoredClock nanoClock, Supplier tracerProvider, @Nullable File activationEventsFile, @Nullable File jfrFile) { this.config = config; this.tracerProvider = tracerProvider; + //TODO: replace with co.elastic.otel.util.ExecutorUtils this.scheduler = Executors.newSingleThreadScheduledExecutor( runnable -> { @@ -211,7 +212,7 @@ public void translateTo( result.setName("elastic-otel-inferred-spans"); return result; }); - this.nanoClock = nanoClock; + this.clock = nanoClock; this.eventBuffer = createRingBuffer(); this.sequence = new Sequence(); // tells the ring buffer to not override slots which have not been read yet @@ -726,7 +727,7 @@ void copyFromFiles(Path activationEvents, Path traces) throws IOException { } public void start() { - scheduler.scheduleAtFixedRate(nanoClock::periodicCleanup, 500, 500, TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate(clock::periodicCleanup, 500, 500, TimeUnit.MILLISECONDS); scheduler.submit(this); } @@ -795,8 +796,8 @@ int getProfilingSessions() { return profilingSessions; } - public NanoClock getClock() { - return nanoClock; + public SpanAnchoredClock getClock() { + return clock; } public static class StackTraceEvent implements Comparable { @@ -854,7 +855,7 @@ public void activation( long threadId, @Nullable Span previousContext, long nanoTime, - NanoClock clock) { + SpanAnchoredClock clock) { set(context, threadId, true, previousContext, nanoTime, clock); } @@ -863,7 +864,7 @@ public void deactivation( long threadId, @Nullable Span previousContext, long nanoTime, - NanoClock clock) { + SpanAnchoredClock clock) { set(context, threadId, false, previousContext, nanoTime, clock); } @@ -873,7 +874,7 @@ private void set( boolean activation, @Nullable Span previousContext, long nanoTime, - NanoClock clock) { + SpanAnchoredClock clock) { TraceContext.serialize( traceContext.getSpanContext(), clock.getAnchor(traceContext), traceContextBuffer); this.threadId = threadId; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java similarity index 79% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java rename to inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java index 0c44d6303..0f16f6cf0 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredNanoClock.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java @@ -23,7 +23,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.trace.ReadWriteSpan; -public class SpanAnchoredNanoClock implements NanoClock { +public class SpanAnchoredClock { private final WeakConcurrentMap nanoTimeOffsetMap = new WeakConcurrentMap<>(false); public void onSpanStart(ReadWriteSpan started, Context parentContext) { @@ -40,22 +40,28 @@ public void onSpanStart(ReadWriteSpan started, Context parentContext) { } } - @Override public long nanoTime() { return System.nanoTime(); } - @Override + /** + * Returns a value which allows to translate timestamps obtained via {@link #nanoTime()} + * to absolute epoche time stamps based on the start-time of the given span. + *

    + * This anchor value can be used in {@link #toEpochNanos(long, long)} to perform the translation. + */ public long getAnchor(Span span) { return nanoTimeOffsetMap.get(span); } - @Override + /** + * Translates a timestamp obtained via {@link #nanoTime()} with the help of an anchor obtaines via {@link #getAnchor(Span)} + * to an absolute nano-precision epoch timestamp. + */ public long toEpochNanos(long anchor, long recordedNanoTime) { return recordedNanoTime + anchor; } - @Override public void periodicCleanup() { nanoTimeOffsetMap.expungeStaleEntries(); } diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java index 7097a08bd..8726c898c 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java @@ -51,6 +51,7 @@ public int getSimpleClassNameOffset() { } public void appendFileName(StringBuilder replaceBuilder) { + final String unknownCodeSource = ""; if (className != null) { int fileNameEnd = className.indexOf('$'); if (fileNameEnd < 0) { @@ -61,10 +62,10 @@ public void appendFileName(StringBuilder replaceBuilder) { replaceBuilder.append(className, classNameStart + 1, fileNameEnd); replaceBuilder.append(".java"); } else { - replaceBuilder.append(""); + replaceBuilder.append(unknownCodeSource); } } else { - replaceBuilder.append(""); + replaceBuilder.append(unknownCodeSource); } } diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java index fb5355178..26e7d13ff 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java @@ -30,7 +30,7 @@ /** * A mutable (and therefore recyclable) class storing the relevant bits of {@link SpanContext} for * generating inferred spans. Also stores a clock-anchor for the corresponding span obtained via - * {@link NanoClock#getAnchor(Span)}. + * {@link SpanAnchoredClock#getAnchor(Span)}. */ public class TraceContext implements Recyclable { diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java index 4e483894c..42f23c9fd 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java +++ b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java @@ -51,7 +51,7 @@ class CallTreeSpanifyTest { @DisabledOnOs(OS.WINDOWS) @DisabledOnAppleSilicon void testSpanification() throws Exception { - FixedNanoClock nanoClock = new FixedNanoClock(); + FixedClock nanoClock = new FixedClock(); try (ProfilerTestSetup setup = ProfilerTestSetup.create( config -> config.clock(nanoClock).startScheduledProfiling(false))) { @@ -93,7 +93,7 @@ void testSpanification() throws Exception { @Test void testCallTreeWithActiveSpan() { - FixedNanoClock nanoClock = new FixedNanoClock(); + FixedClock nanoClock = new FixedClock(); String traceId = "0af7651916cd43dd8448eb211c80319c"; String rootSpanId = "b7ad6b7169203331"; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java index a6c7aae35..1af49612f 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java +++ b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java @@ -55,11 +55,11 @@ class CallTreeTest { private ProfilerTestSetup profilerSetup; - private FixedNanoClock nanoClock; + private FixedClock nanoClock; @BeforeEach void setUp() { - nanoClock = new FixedNanoClock(); + nanoClock = new FixedClock(); // disable scheduled profiling to not interfere with this test profilerSetup = ProfilerTestSetup.create(config -> config.clock(nanoClock).startScheduledProfiling(false)); @@ -139,9 +139,9 @@ void testTwoDistinctInvocationsOfMethodBShouldNotBeFoldedIntoOne() throws Except assertCallTree( new String[] {" bb bb", "aaaaaa"}, new Object[][] { - {"a", 6}, - {" b", 2}, - {" b", 2} + {"a", 6}, + {" b", 2}, + {" b", 2} }); } @@ -150,14 +150,14 @@ void testBasicCallTree() throws Exception { assertCallTree( new String[] {" cc ", " bbb", "aaaa"}, new Object[][] { - {"a", 4}, - {" b", 3}, - {" c", 2} + {"a", 4}, + {" b", 3}, + {" c", 2} }, new Object[][] { - {"a", 3}, - {" b", 2}, - {" c", 1} + {"a", 3}, + {" b", 2}, + {" c", 1} }); } @@ -166,14 +166,14 @@ void testShouldNotCreateInferredSpansForPillarsAndLeafShouldHaveStacktrace() thr assertCallTree( new String[] {" dd ", " cc ", " bb ", "aaaa"}, new Object[][] { - {"a", 4}, - {" b", 2}, - {" c", 2}, - {" d", 2} + {"a", 4}, + {" b", 2}, + {" c", 2}, + {" d", 2} }, new Object[][] { - {"a", 3}, - {" d", 1, List.of("c", "b")} + {"a", 3}, + {" d", 1, List.of("c", "b")} }); } @@ -188,10 +188,10 @@ void testSameTopOfStackDifferentBottom() throws Exception { assertCallTree( new String[] {"cccc", "aabb"}, new Object[][] { - {"a", 2}, - {" c", 2}, - {"b", 2}, - {" c", 2}, + {"a", 2}, + {" c", 2}, + {"b", 2}, + {" c", 2}, }); } @@ -200,12 +200,12 @@ void testStackTraceWithRecursion() throws Exception { assertCallTree( new String[] {"bbccbbcc", "bbbbbbbb", "aaaaaaaa"}, new Object[][] { - {"a", 8}, - {" b", 8}, - {" b", 2}, - {" c", 2}, - {" b", 2}, - {" c", 2}, + {"a", 8}, + {" b", 8}, + {" b", 2}, + {" c", 2}, + {" b", 2}, + {" c", 2}, }); } @@ -214,11 +214,11 @@ void testFirstInferredSpanShouldHaveNoStackTrace() throws Exception { assertCallTree( new String[] {"bb", "aa"}, new Object[][] { - {"a", 2}, - {" b", 2}, + {"a", 2}, + {" b", 2}, }, new Object[][] { - {"b", 1}, + {"b", 1}, }); } @@ -227,19 +227,19 @@ void testCallTreeWithSpanActivations() throws Exception { assertCallTree( new String[] {" cc ee ", " bbb dd ", " a aaaaaa a ", "1 2 2 1"}, new Object[][] { - {"a", 8}, - {" b", 3}, - {" c", 2}, - {" d", 2}, - {" e", 2}, + {"a", 8}, + {" b", 3}, + {" c", 2}, + {" d", 2}, + {" e", 2}, }, new Object[][] { - {"1", 11}, - {" a", 9}, - {" 2", 7}, - {" b", 2}, - {" c", 1}, - {" e", 1, List.of("d")}, + {"1", 11}, + {" a", 9}, + {" 2", 7}, + {" b", 2}, + {" c", 1}, + {" e", 1, List.of("d")}, }); } @@ -255,25 +255,25 @@ void testCallTreeWithSpanActivations() throws Exception { void testDeactivationBeforeEnd() throws Exception { assertCallTree( new String[] { - " dd ", - " cccc c ", - " bbbb bb ", // <- deactivation for span 2 happens before b and c ends - " a aaaa aa ", // that means b and c must have started before 2 has been activated - "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 + " dd ", + " cccc c ", + " bbbb bb ", // <- deactivation for span 2 happens before b and c ends + " a aaaa aa ", // that means b and c must have started before 2 has been activated + "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 }, new Object[][] { - {"a", 7}, - {" b", 6}, - {" c", 5}, - {" d", 2}, + {"a", 7}, + {" b", 6}, + {" c", 5}, + {" d", 2}, }, new Object[][] { - {"1", 10}, - {" a", 8}, - {" b", 7}, - {" c", 6}, - {" 2", 5}, - {" d", 1}, + {"1", 10}, + {" a", 8}, + {" b", 7}, + {" c", 6}, + {" 2", 5}, + {" d", 1}, }); } @@ -288,15 +288,15 @@ void testDectivationBeforeEnd2() throws Exception { assertCallTree( new String[] {" bbbb b ", " a aaaa a a a ", "1 2 2 3 3 1"}, new Object[][] { - {"a", 8}, - {" b", 5}, + {"a", 8}, + {" b", 5}, }, new Object[][] { - {"1", 13}, - {" a", 11}, - {" b", 6}, - {" 2", 5}, - {" 3", 2}, + {"1", 13}, + {" a", 11}, + {" b", 6}, + {" 2", 5}, + {" 3", 2}, }); } @@ -313,15 +313,15 @@ void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations() throws E assertCallTree( new String[] {" c c ", " b b ", "a a a aa", " 1 1 2 2 "}, new Object[][] { - {"a", 5}, - {" b", 2}, - {" c", 2}, + {"a", 5}, + {" b", 2}, + {" c", 2}, }, new Object[][] { - {"a", 9}, - {" 1", 2}, - {" c", 3, List.of("b")}, - {" 2", 2}, + {"a", 9}, + {" 1", 2}, + {" c", 3, List.of("b")}, + {" 2", 2}, }); assertThat(spans.get("a").getLinks()) .hasSize(1) @@ -346,16 +346,16 @@ void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations_Nested() t assertCallTree( new String[] {" c c ", " b b ", "a a a aa", " 1 1 23 32 "}, new Object[][] { - {"a", 5}, - {" b", 2}, - {" c", 2}, + {"a", 5}, + {" b", 2}, + {" c", 2}, }, new Object[][] { - {"a", 11}, - {" 1", 2}, - {" c", 4, List.of("b")}, - {" 2", 4}, - {" 3", 2}, + {"a", 11}, + {" 1", 2}, + {" c", 4, List.of("b")}, + {" 2", 4}, + {" 3", 2}, }); assertThat(spans.get("a").getLinks()) .hasSize(1) @@ -376,13 +376,13 @@ void testActivationAfterMethodEnds() throws Exception { assertCallTree( new String[] {"bb ", "aa a ", " 1 1"}, new Object[][] { - {"a", 3}, - {" b", 2}, + {"a", 3}, + {" b", 2}, }, new Object[][] { - {"a", 3}, - {" b", 1}, - {" 1", 2} + {"a", 3}, + {" b", 1}, + {" 1", 2} }); } @@ -395,13 +395,13 @@ void testActivationBetweenMethods() throws Exception { assertCallTree( new String[] {"bb ", "aa a", " 11 "}, new Object[][] { - {"a", 3}, - {" b", 2}, + {"a", 3}, + {" b", 2}, }, new Object[][] { - {"a", 4}, - {" b", 1}, - {" 1", 1}, + {"a", 4}, + {" b", 1}, + {" 1", 1}, }); } @@ -415,13 +415,13 @@ void testActivationBetweenMethods_AfterFastMethod() throws Exception { assertCallTree( new String[] {" c ", "bb ", "aa a", " 11 "}, new Object[][] { - {"a", 3}, - {" b", 2}, + {"a", 3}, + {" b", 2}, }, new Object[][] { - {"a", 4}, - {" b", 1}, - {" 1", 1}, + {"a", 4}, + {" b", 1}, + {" 1", 1}, }); } @@ -435,14 +435,14 @@ void testActivationBetweenFastMethods() throws Exception { assertCallTree( new String[] {"c d ", "b b ", "a a a", " 11 22 "}, new Object[][] { - {"a", 3}, - {" b", 2}, + {"a", 3}, + {" b", 2}, }, new Object[][] { - {"a", 6}, - {" b", 3}, - {" 1", 1}, - {" 2", 1}, + {"a", 6}, + {" b", 3}, + {" 1", 1}, + {" 2", 1}, }); } @@ -483,12 +483,12 @@ void testNestedActivation() throws Exception { assertCallTree( new String[] {"a a a", " 12 21 "}, new Object[][] { - {"a", 3}, + {"a", 3}, }, new Object[][] { - {"a", 6}, - {" 1", 4}, - {" 2", 2}, + {"a", 6}, + {" 1", 4}, + {" 2", 2}, }); } @@ -504,16 +504,16 @@ void testNestedActivationAfterMethodEnds_RootChangesToC() throws Exception { assertCallTree( new String[] {" bbb ", " aaa ccc ", "1 23 321"}, new Object[][] { - {"a", 3}, - {" b", 3}, - {"c", 3}, + {"a", 3}, + {" b", 3}, + {"c", 3}, }, new Object[][] { - {"1", 11}, - {" b", 2, List.of("a")}, - {" 2", 6}, - {" 3", 4}, - {" c", 2} + {"1", 11}, + {" b", 2, List.of("a")}, + {" 2", 6}, + {" 3", 4}, + {" c", 2} }); assertThat(spans.get("b").getLinks()).isEmpty(); @@ -530,17 +530,17 @@ void testRegularActivationFollowedByNestedActivationAfterMethodEnds() throws Exc assertCallTree( new String[] {" d ", " b b b ", " a a a ccc ", "1 2 2 34 431"}, new Object[][] { - {"a", 3}, - {" b", 3}, - {"c", 3}, + {"a", 3}, + {" b", 3}, + {"c", 3}, }, new Object[][] { - {"1", 13}, - {" b", 4, List.of("a")}, - {" 2", 2}, - {" 3", 6}, - {" 4", 4}, - {" c", 2} + {"1", 13}, + {" b", 4, List.of("a")}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} }); } @@ -557,18 +557,18 @@ void testNestedActivationAfterMethodEnds_CommonAncestorA() throws Exception { assertCallTree( new String[] {" b b b ccc ", " aa a a aaa a ", "1 2 2 34 43 1"}, new Object[][] { - {"a", 8}, - {" b", 3}, - {" c", 3}, + {"a", 8}, + {" b", 3}, + {" c", 3}, }, new Object[][] { - {"1", 15}, - {" a", 13}, - {" b", 4}, - {" 2", 2}, - {" 3", 6}, - {" 4", 4}, - {" c", 2} + {"1", 15}, + {" a", 13}, + {" b", 4}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} }); assertThat(spans.get("b").getLinks()) @@ -606,15 +606,15 @@ void testActivationAfterMethodEnds_RootChangesToB() throws Exception { assertCallTree( new String[] {" ccc ", " aaa bbb ", "1 2 21"}, new Object[][] { - {"a", 3}, - {"b", 3}, - {" c", 3}, + {"a", 3}, + {"b", 3}, + {" c", 3}, }, new Object[][] { - {"1", 9}, - {" a", 2}, - {" 2", 4}, - {" c", 2, List.of("b")} + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" c", 2, List.of("b")} }); } @@ -629,14 +629,14 @@ void testActivationAfterMethodEnds_RootChangesToB2() throws Exception { assertCallTree( new String[] {" aaa bbb ", "1 2 21"}, new Object[][] { - {"a", 3}, - {"b", 3}, + {"a", 3}, + {"b", 3}, }, new Object[][] { - {"1", 9}, - {" a", 2}, - {" 2", 4}, - {" b", 2} + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" b", 2} }); } @@ -668,14 +668,14 @@ void testActivationAfterMethodEnds_SameRootDeeperStack() throws Exception { assertCallTree( new String[] {" ccc ", " aaa aaa ", "1 2 21"}, new Object[][] { - {"a", 6}, - {" c", 3}, + {"a", 6}, + {" c", 3}, }, new Object[][] { - {"1", 9}, - {" a", 6}, - {" 2", 4}, - {" c", 2} + {"1", 9}, + {" a", 6}, + {" 2", 4}, + {" c", 2} }); } @@ -690,14 +690,14 @@ void testActivationBeforeMethodStarts() throws Exception { assertCallTree( new String[] {" bbb ", " a aaa a ", "1 2 2 1"}, new Object[][] { - {"a", 5}, - {" b", 3}, + {"a", 5}, + {" b", 3}, }, new Object[][] { - {"1", 8}, - {" a", 6}, - {" 2", 4}, - {" b", 2} + {"1", 8}, + {" a", 6}, + {" 2", 4}, + {" b", 2} }); } @@ -713,25 +713,25 @@ void testActivationBeforeMethodStarts() throws Exception { void testDectivationAfterEnd() throws Exception { assertCallTree( new String[] { - " dd ", - " c ccc ", - " bb bbb ", // <- deactivation for span 2 happens after b ends - " aaa aaa aa ", // that means b must have ended after 2 has been deactivated - "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 + " dd ", + " c ccc ", + " bb bbb ", // <- deactivation for span 2 happens after b ends + " aaa aaa aa ", // that means b must have ended after 2 has been deactivated + "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 }, new Object[][] { - {"a", 8}, - {" b", 5}, - {" c", 4}, - {" d", 2}, + {"a", 8}, + {" b", 5}, + {" c", 4}, + {" d", 2}, }, new Object[][] { - {"1", 11}, - {" a", 9}, - {" b", 6}, - {" c", 5}, - {" 2", 4}, - {" d", 1}, + {"1", 11}, + {" a", 9}, + {" b", 6}, + {" c", 5}, + {" 2", 4}, + {" d", 1}, }); } @@ -741,9 +741,9 @@ void testCallTreeActivationAsParentOfFastSpan() throws Exception { new String[] {" b ", " aa a aa ", "1 2 2 1"}, new Object[][] {{"a", 5}}, new Object[][] { - {"1", 8}, - {" a", 6}, - {" 2", 2}, + {"1", 8}, + {" a", 6}, + {" 2", 2}, }); } @@ -762,9 +762,9 @@ void testCallTreeActivationAsChildOfFastSpan() throws Exception { new String[] {" c c ", " b b ", " aaa aaa ", "1 22 1"}, new Object[][] {{"a", 6}}, new Object[][] { - {"1", 9}, - {" a", 7}, - {" 2", 1}, + {"1", 9}, + {" a", 7}, + {" 2", 1}, }); } @@ -774,9 +774,9 @@ void testCallTreeActivationAsLeaf() throws Exception { new String[] {" aa aa ", "1 22 1"}, new Object[][] {{"a", 4}}, new Object[][] { - {"1", 7}, - {" a", 5}, - {" 2", 1}, + {"1", 7}, + {" a", 5}, + {" 2", 1}, }); } @@ -786,10 +786,10 @@ void testCallTreeMultipleActivationsAsLeaf() throws Exception { new String[] {" aa aaa aa ", "1 22 33 1"}, new Object[][] {{"a", 7}}, new Object[][] { - {"1", 12}, - {" a", 10}, - {" 2", 1}, - {" 3", 1}, + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, }); } @@ -809,10 +809,10 @@ void testCallTreeMultipleActivationsAsLeafWithExcludedParent() throws Exception new String[] {" b b c c ", " aa aaa aa ", "1 22 33 1"}, new Object[][] {{"a", 7}}, new Object[][] { - {"1", 12}, - {" a", 10}, - {" 2", 1}, - {" 3", 1}, + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, }); } @@ -821,15 +821,15 @@ void testCallTreeMultipleActivationsWithOneChild() throws Exception { assertCallTree( new String[] {" bb ", " aa aaa aa aa ", "1 22 3 3 1"}, new Object[][] { - {"a", 9}, - {" b", 2} + {"a", 9}, + {" b", 2} }, new Object[][] { - {"1", 14}, - {" a", 12}, - {" 2", 1}, - {" 3", 3}, - {" b", 1}, + {"1", 14}, + {" a", 12}, + {" 2", 1}, + {" 3", 3}, + {" b", 1}, }); } @@ -846,12 +846,12 @@ void testNestedActivationBeforeCallTree() throws Exception { assertCallTree( new String[] {" aaa ", "12 2 1"}, new Object[][] { - {"a", 3}, + {"a", 3}, }, new Object[][] { - {"1", 5}, - {" a", 3}, // a is actually a child of the transaction - {" 2", 2}, // 2 is not within the child_ids of a + {"1", 5}, + {" a", 3}, // a is actually a child of the transaction + {" 2", 2}, // 2 is not within the child_ids of a }); } @@ -1001,7 +1001,7 @@ private int getNestingLevel(String spanName) { public static CallTree.Root getCallTree(ProfilerTestSetup profilerSetup, String[] stackTraces) throws Exception { SamplingProfiler profiler = profilerSetup.profiler; - FixedNanoClock nanoClock = (FixedNanoClock) profilerSetup.profiler.getClock(); + FixedClock nanoClock = (FixedClock) profilerSetup.profiler.getClock(); nanoClock.setNanoTime(1); profiler.setProfilingSessionOngoing(true); diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedClock.java similarity index 96% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java rename to inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedClock.java index 608eb033a..68c6970ba 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedNanoClock.java +++ b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedClock.java @@ -22,7 +22,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.sdk.trace.ReadWriteSpan; -public class FixedNanoClock implements NanoClock { +public class FixedClock extends SpanAnchoredClock { private long nanoTime = -1L; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java index 32c7045d9..a3a231397 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java +++ b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java @@ -38,7 +38,7 @@ void testFillQueue() throws Exception { try (ProfilerTestSetup setup = ProfilerTestSetup.create( - config -> config.clock(new FixedNanoClock()).startScheduledProfiling(false))) { + config -> config.clock(new FixedClock()).startScheduledProfiling(false))) { setup.profiler.setProfilingSessionOngoing(true); diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java index 6aa0e8769..e2d45d437 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java +++ b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java @@ -137,7 +137,7 @@ void shouldNotDeleteProvidedFiles() throws Exception { SamplingProfiler otherProfiler = new SamplingProfiler( defaultConfig, - new FixedNanoClock(), + new FixedClock(), () -> sdk.getTracer("my-tracer"), tempFile1.toFile(), tempFile2.toFile()); @@ -255,7 +255,7 @@ void testProfileTransaction() throws Exception { @Test void ensurePeriodicCleanupInvoked() throws Exception { - NanoClock mockClock = Mockito.mock(NanoClock.class); + SpanAnchoredClock mockClock = Mockito.mock(SpanAnchoredClock.class); setupProfiler(config -> config.clock(mockClock)); awaitProfilerStarted(setup.profiler); From f2af511c537442e8ef304861400472f387d31dd6 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Wed, 6 Dec 2023 14:02:30 +0100 Subject: [PATCH 11/18] Spotless --- .../otel/profiler/InferredSpansProcessor.java | 2 +- .../profiler/ProfilingActivationListener.java | 5 +- .../apm/otel/profiler/SamplingProfiler.java | 14 +- .../apm/otel/profiler/SpanAnchoredClock.java | 13 +- .../apm/otel/profiler/CallTreeTest.java | 392 +++++++++--------- 5 files changed, 210 insertions(+), 216 deletions(-) diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java index 3f6b17415..76d00f001 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java @@ -89,7 +89,7 @@ public boolean isEndRequired() { public CompletableResultCode shutdown() { CompletableResultCode result = new CompletableResultCode(); logger.fine("Stopping Inferred Spans Processor"); - //TODO: Replace with co.elastic.otel.util.ExecutorUtils + // TODO: Replace with co.elastic.otel.util.ExecutorUtils Executors.newSingleThreadExecutor() .submit( () -> { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java index fdf876931..b2a0f61e2 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java @@ -36,12 +36,13 @@ public class ProfilingActivationListener implements Closeable { // * happen before anyone accesses any Context // * happen exactly once // The "exactly" once part is why we use a static initializer: - // If an Otel-SDK is created and immediately shutdown again and if we create another SDK afterwards, + // If an Otel-SDK is created and immediately shutdown again and if we create another SDK + // afterwards, // we might accidentally register the wrapper twice ContextStorage.addWrapper(ContextStorageWrapper::new); } - //For testing only + // For testing only static void ensureInitialized() { // does nothing but ensures that the static initializer ran } diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java index 51fc68c2c..441a3a2d1 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java @@ -125,11 +125,7 @@ class SamplingProfiler implements Runnable { public void translateTo( ActivationEvent event, long sequence, Span active, Span previouslyActive) { event.activation( - active, - Thread.currentThread().getId(), - previouslyActive, - clock.nanoTime(), - clock); + active, Thread.currentThread().getId(), previouslyActive, clock.nanoTime(), clock); } }; private final EventTranslatorTwoArg DEACTIVATION_EVENT_TRANSLATOR = @@ -138,11 +134,7 @@ public void translateTo( public void translateTo( ActivationEvent event, long sequence, Span active, Span previouslyActive) { event.deactivation( - active, - Thread.currentThread().getId(), - previouslyActive, - clock.nanoTime(), - clock); + active, Thread.currentThread().getId(), previouslyActive, clock.nanoTime(), clock); } }; // sizeof(ActivationEvent) is 176B so the ring buffer should be around 880KiB @@ -204,7 +196,7 @@ public void translateTo( @Nullable File jfrFile) { this.config = config; this.tracerProvider = tracerProvider; - //TODO: replace with co.elastic.otel.util.ExecutorUtils + // TODO: replace with co.elastic.otel.util.ExecutorUtils this.scheduler = Executors.newSingleThreadScheduledExecutor( runnable -> { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java index 0f16f6cf0..ebbea7bb8 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java @@ -45,18 +45,19 @@ public long nanoTime() { } /** - * Returns a value which allows to translate timestamps obtained via {@link #nanoTime()} - * to absolute epoche time stamps based on the start-time of the given span. - *

    - * This anchor value can be used in {@link #toEpochNanos(long, long)} to perform the translation. + * Returns a value which allows to translate timestamps obtained via {@link #nanoTime()} to + * absolute epoche time stamps based on the start-time of the given span. + * + *

    This anchor value can be used in {@link #toEpochNanos(long, long)} to perform the + * translation. */ public long getAnchor(Span span) { return nanoTimeOffsetMap.get(span); } /** - * Translates a timestamp obtained via {@link #nanoTime()} with the help of an anchor obtaines via {@link #getAnchor(Span)} - * to an absolute nano-precision epoch timestamp. + * Translates a timestamp obtained via {@link #nanoTime()} with the help of an anchor obtaines via + * {@link #getAnchor(Span)} to an absolute nano-precision epoch timestamp. */ public long toEpochNanos(long anchor, long recordedNanoTime) { return recordedNanoTime + anchor; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java index 1af49612f..496fbf0ab 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java +++ b/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java @@ -139,9 +139,9 @@ void testTwoDistinctInvocationsOfMethodBShouldNotBeFoldedIntoOne() throws Except assertCallTree( new String[] {" bb bb", "aaaaaa"}, new Object[][] { - {"a", 6}, - {" b", 2}, - {" b", 2} + {"a", 6}, + {" b", 2}, + {" b", 2} }); } @@ -150,14 +150,14 @@ void testBasicCallTree() throws Exception { assertCallTree( new String[] {" cc ", " bbb", "aaaa"}, new Object[][] { - {"a", 4}, - {" b", 3}, - {" c", 2} + {"a", 4}, + {" b", 3}, + {" c", 2} }, new Object[][] { - {"a", 3}, - {" b", 2}, - {" c", 1} + {"a", 3}, + {" b", 2}, + {" c", 1} }); } @@ -166,14 +166,14 @@ void testShouldNotCreateInferredSpansForPillarsAndLeafShouldHaveStacktrace() thr assertCallTree( new String[] {" dd ", " cc ", " bb ", "aaaa"}, new Object[][] { - {"a", 4}, - {" b", 2}, - {" c", 2}, - {" d", 2} + {"a", 4}, + {" b", 2}, + {" c", 2}, + {" d", 2} }, new Object[][] { - {"a", 3}, - {" d", 1, List.of("c", "b")} + {"a", 3}, + {" d", 1, List.of("c", "b")} }); } @@ -188,10 +188,10 @@ void testSameTopOfStackDifferentBottom() throws Exception { assertCallTree( new String[] {"cccc", "aabb"}, new Object[][] { - {"a", 2}, - {" c", 2}, - {"b", 2}, - {" c", 2}, + {"a", 2}, + {" c", 2}, + {"b", 2}, + {" c", 2}, }); } @@ -200,12 +200,12 @@ void testStackTraceWithRecursion() throws Exception { assertCallTree( new String[] {"bbccbbcc", "bbbbbbbb", "aaaaaaaa"}, new Object[][] { - {"a", 8}, - {" b", 8}, - {" b", 2}, - {" c", 2}, - {" b", 2}, - {" c", 2}, + {"a", 8}, + {" b", 8}, + {" b", 2}, + {" c", 2}, + {" b", 2}, + {" c", 2}, }); } @@ -214,11 +214,11 @@ void testFirstInferredSpanShouldHaveNoStackTrace() throws Exception { assertCallTree( new String[] {"bb", "aa"}, new Object[][] { - {"a", 2}, - {" b", 2}, + {"a", 2}, + {" b", 2}, }, new Object[][] { - {"b", 1}, + {"b", 1}, }); } @@ -227,19 +227,19 @@ void testCallTreeWithSpanActivations() throws Exception { assertCallTree( new String[] {" cc ee ", " bbb dd ", " a aaaaaa a ", "1 2 2 1"}, new Object[][] { - {"a", 8}, - {" b", 3}, - {" c", 2}, - {" d", 2}, - {" e", 2}, + {"a", 8}, + {" b", 3}, + {" c", 2}, + {" d", 2}, + {" e", 2}, }, new Object[][] { - {"1", 11}, - {" a", 9}, - {" 2", 7}, - {" b", 2}, - {" c", 1}, - {" e", 1, List.of("d")}, + {"1", 11}, + {" a", 9}, + {" 2", 7}, + {" b", 2}, + {" c", 1}, + {" e", 1, List.of("d")}, }); } @@ -255,25 +255,25 @@ void testCallTreeWithSpanActivations() throws Exception { void testDeactivationBeforeEnd() throws Exception { assertCallTree( new String[] { - " dd ", - " cccc c ", - " bbbb bb ", // <- deactivation for span 2 happens before b and c ends - " a aaaa aa ", // that means b and c must have started before 2 has been activated - "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 + " dd ", + " cccc c ", + " bbbb bb ", // <- deactivation for span 2 happens before b and c ends + " a aaaa aa ", // that means b and c must have started before 2 has been activated + "1 2 2 1" // but we saw the first stack trace of b only after the activation of 2 }, new Object[][] { - {"a", 7}, - {" b", 6}, - {" c", 5}, - {" d", 2}, + {"a", 7}, + {" b", 6}, + {" c", 5}, + {" d", 2}, }, new Object[][] { - {"1", 10}, - {" a", 8}, - {" b", 7}, - {" c", 6}, - {" 2", 5}, - {" d", 1}, + {"1", 10}, + {" a", 8}, + {" b", 7}, + {" c", 6}, + {" 2", 5}, + {" d", 1}, }); } @@ -288,15 +288,15 @@ void testDectivationBeforeEnd2() throws Exception { assertCallTree( new String[] {" bbbb b ", " a aaaa a a a ", "1 2 2 3 3 1"}, new Object[][] { - {"a", 8}, - {" b", 5}, + {"a", 8}, + {" b", 5}, }, new Object[][] { - {"1", 13}, - {" a", 11}, - {" b", 6}, - {" 2", 5}, - {" 3", 2}, + {"1", 13}, + {" a", 11}, + {" b", 6}, + {" 2", 5}, + {" 3", 2}, }); } @@ -313,15 +313,15 @@ void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations() throws E assertCallTree( new String[] {" c c ", " b b ", "a a a aa", " 1 1 2 2 "}, new Object[][] { - {"a", 5}, - {" b", 2}, - {" c", 2}, + {"a", 5}, + {" b", 2}, + {" c", 2}, }, new Object[][] { - {"a", 9}, - {" 1", 2}, - {" c", 3, List.of("b")}, - {" 2", 2}, + {"a", 9}, + {" 1", 2}, + {" c", 3, List.of("b")}, + {" 2", 2}, }); assertThat(spans.get("a").getLinks()) .hasSize(1) @@ -346,16 +346,16 @@ void testDectivationBeforeEnd_DontStealChildIdsOfUnrelatedActivations_Nested() t assertCallTree( new String[] {" c c ", " b b ", "a a a aa", " 1 1 23 32 "}, new Object[][] { - {"a", 5}, - {" b", 2}, - {" c", 2}, + {"a", 5}, + {" b", 2}, + {" c", 2}, }, new Object[][] { - {"a", 11}, - {" 1", 2}, - {" c", 4, List.of("b")}, - {" 2", 4}, - {" 3", 2}, + {"a", 11}, + {" 1", 2}, + {" c", 4, List.of("b")}, + {" 2", 4}, + {" 3", 2}, }); assertThat(spans.get("a").getLinks()) .hasSize(1) @@ -376,13 +376,13 @@ void testActivationAfterMethodEnds() throws Exception { assertCallTree( new String[] {"bb ", "aa a ", " 1 1"}, new Object[][] { - {"a", 3}, - {" b", 2}, + {"a", 3}, + {" b", 2}, }, new Object[][] { - {"a", 3}, - {" b", 1}, - {" 1", 2} + {"a", 3}, + {" b", 1}, + {" 1", 2} }); } @@ -395,13 +395,13 @@ void testActivationBetweenMethods() throws Exception { assertCallTree( new String[] {"bb ", "aa a", " 11 "}, new Object[][] { - {"a", 3}, - {" b", 2}, + {"a", 3}, + {" b", 2}, }, new Object[][] { - {"a", 4}, - {" b", 1}, - {" 1", 1}, + {"a", 4}, + {" b", 1}, + {" 1", 1}, }); } @@ -415,13 +415,13 @@ void testActivationBetweenMethods_AfterFastMethod() throws Exception { assertCallTree( new String[] {" c ", "bb ", "aa a", " 11 "}, new Object[][] { - {"a", 3}, - {" b", 2}, + {"a", 3}, + {" b", 2}, }, new Object[][] { - {"a", 4}, - {" b", 1}, - {" 1", 1}, + {"a", 4}, + {" b", 1}, + {" 1", 1}, }); } @@ -435,14 +435,14 @@ void testActivationBetweenFastMethods() throws Exception { assertCallTree( new String[] {"c d ", "b b ", "a a a", " 11 22 "}, new Object[][] { - {"a", 3}, - {" b", 2}, + {"a", 3}, + {" b", 2}, }, new Object[][] { - {"a", 6}, - {" b", 3}, - {" 1", 1}, - {" 2", 1}, + {"a", 6}, + {" b", 3}, + {" 1", 1}, + {" 2", 1}, }); } @@ -483,12 +483,12 @@ void testNestedActivation() throws Exception { assertCallTree( new String[] {"a a a", " 12 21 "}, new Object[][] { - {"a", 3}, + {"a", 3}, }, new Object[][] { - {"a", 6}, - {" 1", 4}, - {" 2", 2}, + {"a", 6}, + {" 1", 4}, + {" 2", 2}, }); } @@ -504,16 +504,16 @@ void testNestedActivationAfterMethodEnds_RootChangesToC() throws Exception { assertCallTree( new String[] {" bbb ", " aaa ccc ", "1 23 321"}, new Object[][] { - {"a", 3}, - {" b", 3}, - {"c", 3}, + {"a", 3}, + {" b", 3}, + {"c", 3}, }, new Object[][] { - {"1", 11}, - {" b", 2, List.of("a")}, - {" 2", 6}, - {" 3", 4}, - {" c", 2} + {"1", 11}, + {" b", 2, List.of("a")}, + {" 2", 6}, + {" 3", 4}, + {" c", 2} }); assertThat(spans.get("b").getLinks()).isEmpty(); @@ -530,17 +530,17 @@ void testRegularActivationFollowedByNestedActivationAfterMethodEnds() throws Exc assertCallTree( new String[] {" d ", " b b b ", " a a a ccc ", "1 2 2 34 431"}, new Object[][] { - {"a", 3}, - {" b", 3}, - {"c", 3}, + {"a", 3}, + {" b", 3}, + {"c", 3}, }, new Object[][] { - {"1", 13}, - {" b", 4, List.of("a")}, - {" 2", 2}, - {" 3", 6}, - {" 4", 4}, - {" c", 2} + {"1", 13}, + {" b", 4, List.of("a")}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} }); } @@ -557,18 +557,18 @@ void testNestedActivationAfterMethodEnds_CommonAncestorA() throws Exception { assertCallTree( new String[] {" b b b ccc ", " aa a a aaa a ", "1 2 2 34 43 1"}, new Object[][] { - {"a", 8}, - {" b", 3}, - {" c", 3}, + {"a", 8}, + {" b", 3}, + {" c", 3}, }, new Object[][] { - {"1", 15}, - {" a", 13}, - {" b", 4}, - {" 2", 2}, - {" 3", 6}, - {" 4", 4}, - {" c", 2} + {"1", 15}, + {" a", 13}, + {" b", 4}, + {" 2", 2}, + {" 3", 6}, + {" 4", 4}, + {" c", 2} }); assertThat(spans.get("b").getLinks()) @@ -606,15 +606,15 @@ void testActivationAfterMethodEnds_RootChangesToB() throws Exception { assertCallTree( new String[] {" ccc ", " aaa bbb ", "1 2 21"}, new Object[][] { - {"a", 3}, - {"b", 3}, - {" c", 3}, + {"a", 3}, + {"b", 3}, + {" c", 3}, }, new Object[][] { - {"1", 9}, - {" a", 2}, - {" 2", 4}, - {" c", 2, List.of("b")} + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" c", 2, List.of("b")} }); } @@ -629,14 +629,14 @@ void testActivationAfterMethodEnds_RootChangesToB2() throws Exception { assertCallTree( new String[] {" aaa bbb ", "1 2 21"}, new Object[][] { - {"a", 3}, - {"b", 3}, + {"a", 3}, + {"b", 3}, }, new Object[][] { - {"1", 9}, - {" a", 2}, - {" 2", 4}, - {" b", 2} + {"1", 9}, + {" a", 2}, + {" 2", 4}, + {" b", 2} }); } @@ -668,14 +668,14 @@ void testActivationAfterMethodEnds_SameRootDeeperStack() throws Exception { assertCallTree( new String[] {" ccc ", " aaa aaa ", "1 2 21"}, new Object[][] { - {"a", 6}, - {" c", 3}, + {"a", 6}, + {" c", 3}, }, new Object[][] { - {"1", 9}, - {" a", 6}, - {" 2", 4}, - {" c", 2} + {"1", 9}, + {" a", 6}, + {" 2", 4}, + {" c", 2} }); } @@ -690,14 +690,14 @@ void testActivationBeforeMethodStarts() throws Exception { assertCallTree( new String[] {" bbb ", " a aaa a ", "1 2 2 1"}, new Object[][] { - {"a", 5}, - {" b", 3}, + {"a", 5}, + {" b", 3}, }, new Object[][] { - {"1", 8}, - {" a", 6}, - {" 2", 4}, - {" b", 2} + {"1", 8}, + {" a", 6}, + {" 2", 4}, + {" b", 2} }); } @@ -713,25 +713,25 @@ void testActivationBeforeMethodStarts() throws Exception { void testDectivationAfterEnd() throws Exception { assertCallTree( new String[] { - " dd ", - " c ccc ", - " bb bbb ", // <- deactivation for span 2 happens after b ends - " aaa aaa aa ", // that means b must have ended after 2 has been deactivated - "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 + " dd ", + " c ccc ", + " bb bbb ", // <- deactivation for span 2 happens after b ends + " aaa aaa aa ", // that means b must have ended after 2 has been deactivated + "1 2 2 1" // but we saw the last stack trace of b before the deactivation of 2 }, new Object[][] { - {"a", 8}, - {" b", 5}, - {" c", 4}, - {" d", 2}, + {"a", 8}, + {" b", 5}, + {" c", 4}, + {" d", 2}, }, new Object[][] { - {"1", 11}, - {" a", 9}, - {" b", 6}, - {" c", 5}, - {" 2", 4}, - {" d", 1}, + {"1", 11}, + {" a", 9}, + {" b", 6}, + {" c", 5}, + {" 2", 4}, + {" d", 1}, }); } @@ -741,9 +741,9 @@ void testCallTreeActivationAsParentOfFastSpan() throws Exception { new String[] {" b ", " aa a aa ", "1 2 2 1"}, new Object[][] {{"a", 5}}, new Object[][] { - {"1", 8}, - {" a", 6}, - {" 2", 2}, + {"1", 8}, + {" a", 6}, + {" 2", 2}, }); } @@ -762,9 +762,9 @@ void testCallTreeActivationAsChildOfFastSpan() throws Exception { new String[] {" c c ", " b b ", " aaa aaa ", "1 22 1"}, new Object[][] {{"a", 6}}, new Object[][] { - {"1", 9}, - {" a", 7}, - {" 2", 1}, + {"1", 9}, + {" a", 7}, + {" 2", 1}, }); } @@ -774,9 +774,9 @@ void testCallTreeActivationAsLeaf() throws Exception { new String[] {" aa aa ", "1 22 1"}, new Object[][] {{"a", 4}}, new Object[][] { - {"1", 7}, - {" a", 5}, - {" 2", 1}, + {"1", 7}, + {" a", 5}, + {" 2", 1}, }); } @@ -786,10 +786,10 @@ void testCallTreeMultipleActivationsAsLeaf() throws Exception { new String[] {" aa aaa aa ", "1 22 33 1"}, new Object[][] {{"a", 7}}, new Object[][] { - {"1", 12}, - {" a", 10}, - {" 2", 1}, - {" 3", 1}, + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, }); } @@ -809,10 +809,10 @@ void testCallTreeMultipleActivationsAsLeafWithExcludedParent() throws Exception new String[] {" b b c c ", " aa aaa aa ", "1 22 33 1"}, new Object[][] {{"a", 7}}, new Object[][] { - {"1", 12}, - {" a", 10}, - {" 2", 1}, - {" 3", 1}, + {"1", 12}, + {" a", 10}, + {" 2", 1}, + {" 3", 1}, }); } @@ -821,15 +821,15 @@ void testCallTreeMultipleActivationsWithOneChild() throws Exception { assertCallTree( new String[] {" bb ", " aa aaa aa aa ", "1 22 3 3 1"}, new Object[][] { - {"a", 9}, - {" b", 2} + {"a", 9}, + {" b", 2} }, new Object[][] { - {"1", 14}, - {" a", 12}, - {" 2", 1}, - {" 3", 3}, - {" b", 1}, + {"1", 14}, + {" a", 12}, + {" 2", 1}, + {" 3", 3}, + {" b", 1}, }); } @@ -846,12 +846,12 @@ void testNestedActivationBeforeCallTree() throws Exception { assertCallTree( new String[] {" aaa ", "12 2 1"}, new Object[][] { - {"a", 3}, + {"a", 3}, }, new Object[][] { - {"1", 5}, - {" a", 3}, // a is actually a child of the transaction - {" 2", 2}, // 2 is not within the child_ids of a + {"1", 5}, + {" a", 3}, // a is actually a child of the transaction + {" 2", 2}, // 2 is not within the child_ids of a }); } From 6101aa1b642fba93e39ec8361801fa745c8a1a81 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Wed, 6 Dec 2023 14:56:43 +0100 Subject: [PATCH 12/18] Added comment explaining activation listeners List rationale --- .../apm/otel/profiler/ProfilingActivationListener.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java index b2a0f61e2..953a41e28 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java +++ b/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java @@ -37,8 +37,7 @@ public class ProfilingActivationListener implements Closeable { // * happen exactly once // The "exactly" once part is why we use a static initializer: // If an Otel-SDK is created and immediately shutdown again and if we create another SDK - // afterwards, - // we might accidentally register the wrapper twice + // afterwards, we might accidentally register the wrapper twice ContextStorage.addWrapper(ContextStorageWrapper::new); } @@ -47,6 +46,9 @@ static void ensureInitialized() { // does nothing but ensures that the static initializer ran } + // In normal use-cases there is only one ProfilingActivationListener active or zero + // (e.g. after SDK shutdown). However, in theory nothing prevents users from starting + // two SDKs at the same time, so it is safest to use a List here. private static volatile List activeListeners = Collections.emptyList(); From 23d9763d52845a4c005819ec7d206dc6dc3f8376 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Thu, 7 Dec 2023 12:05:08 +0100 Subject: [PATCH 13/18] Centralized a few dependencies which are likely to be shared --- gradle/libs.versions.toml | 4 ++++ inferred-spans-otel/build.gradle.kts | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e13c1a1f1..9a45d778e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,10 @@ opentelemetryInstrumentationAlphaBom = { group = "io.opentelemetry.instrumentati awsContribResources = { group = "io.opentelemetry.contrib", name = "opentelemetry-aws-resources", version.ref = "opentelemetryContribAlpha" } contribResources = { group = "io.opentelemetry.contrib", name = "opentelemetry-resource-providers", version.ref = "opentelemetryContribAlpha" } +assertJ-core = "org.assertj:assertj-core:3.24.2" +awaitility = "org.awaitility:awaitility:4.2.0" +findbugs-jsr305 = "com.google.code.findbugs:jsr305:3.0.0" + [bundles] [plugins] diff --git a/inferred-spans-otel/build.gradle.kts b/inferred-spans-otel/build.gradle.kts index d921d61a9..9018d6533 100644 --- a/inferred-spans-otel/build.gradle.kts +++ b/inferred-spans-otel/build.gradle.kts @@ -4,17 +4,17 @@ plugins { dependencies { compileOnly("io.opentelemetry:opentelemetry-sdk") - compileOnly("com.google.code.findbugs:jsr305:3.0.0") + compileOnly(libs.findbugs.jsr305) implementation("com.lmax:disruptor:3.4.4") implementation("org.jctools:jctools-core:4.0.1") implementation("com.blogspot.mydailyjava:weak-lock-free:0.18") - testCompileOnly("com.google.code.findbugs:jsr305:3.0.0") + testCompileOnly(libs.findbugs.jsr305) testImplementation("io.opentelemetry:opentelemetry-sdk") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") - testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation(libs.assertJ.core) + testImplementation(libs.awaitility) testImplementation("org.kohsuke:github-api:1.133") - testImplementation("org.awaitility:awaitility:4.2.0") testImplementation("org.apache.commons:commons-compress:1.21") testImplementation("tools.profiler:async-profiler:1.8.3") } From 3192aaac2cb6a4a423852dc8e62917ae3e781fa4 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Mon, 11 Dec 2023 11:38:37 +0100 Subject: [PATCH 14/18] Removed -otel suffix, removed apm in package name --- build.gradle | 2 +- .../build.gradle.kts | 0 .../co/elastic}/otel/profiler/CallTree.java | 12 ++++++------ .../profiler/InferredSpansConfiguration.java | 4 ++-- .../otel/profiler/InferredSpansProcessor.java | 2 +- .../profiler/InferredSpansProcessorBuilder.java | 4 ++-- .../profiler/ProfilingActivationListener.java | 4 ++-- .../otel/profiler/SamplingProfiler.java | 14 +++++++------- .../otel/profiler/SpanAnchoredClock.java | 2 +- .../co/elastic}/otel/profiler/StackFrame.java | 2 +- .../elastic}/otel/profiler/ThreadMatcher.java | 2 +- .../co/elastic}/otel/profiler/TraceContext.java | 8 ++++---- .../profiler/asyncprofiler/AsyncProfiler.java | 2 +- .../profiler/asyncprofiler/BufferedFile.java | 4 ++-- .../otel/profiler/asyncprofiler/JfrParser.java | 16 ++++++++-------- .../asyncprofiler/ResourceExtractionUtil.java | 2 +- .../profiler/collections/CollectionUtil.java | 2 +- .../otel/profiler/collections/Hashing.java | 2 +- .../profiler/collections/Int2IntHashMap.java | 2 +- .../profiler/collections/Int2ObjectHashMap.java | 6 +++--- .../profiler/collections/IntIntConsumer.java | 2 +- .../profiler/collections/Long2LongHashMap.java | 6 +++--- .../collections/Long2ObjectHashMap.java | 6 +++--- .../otel/profiler/collections/LongHashSet.java | 6 +++--- .../otel/profiler/collections/LongList.java | 2 +- .../profiler/collections/LongLongConsumer.java | 2 +- .../otel/profiler/collections/package-info.java | 2 +- .../otel/profiler/config/WildcardMatcher.java | 2 +- .../profiler/pooling/AbstractObjectPool.java | 2 +- .../otel/profiler/pooling/Allocator.java | 2 +- .../otel/profiler/pooling/ObjectPool.java | 2 +- .../profiler/pooling/QueueBasedObjectPool.java | 2 +- .../otel/profiler/pooling/Recyclable.java | 2 +- .../otel/profiler/pooling/Resetter.java | 2 +- .../elastic}/otel/profiler/util/ByteUtils.java | 2 +- .../elastic}/otel/profiler/util/HexUtils.java | 2 +- .../otel/profiler/util/ThreadUtils.java | 2 +- .../libasyncProfiler-linux-aarch64.so | Bin .../asyncprofiler/libasyncProfiler-linux-arm.so | Bin .../asyncprofiler/libasyncProfiler-linux-x64.so | Bin .../asyncprofiler/libasyncProfiler-linux-x86.so | Bin .../asyncprofiler/libasyncProfiler-macos-x64.so | Bin .../otel/profiler/CallTreeSpanifyTest.java | 6 +++--- .../co/elastic}/otel/profiler/CallTreeTest.java | 6 +++--- .../co/elastic}/otel/profiler/FixedClock.java | 2 +- .../otel/profiler/ProfilerTestSetup.java | 2 +- .../profiler/SamplingProfilerQueueTest.java | 4 ++-- .../otel/profiler/SamplingProfilerReplay.java | 2 +- .../otel/profiler/SamplingProfilerTest.java | 4 ++-- .../otel/profiler/ThreadMatcherTest.java | 2 +- .../asyncprofiler/AsyncProfilerTest.java | 6 +++--- .../asyncprofiler/AsyncProfilerUpgrader.java | 5 ++--- .../profiler/asyncprofiler/JfrParserTest.java | 6 +++--- .../profiler/util/DisabledOnAppleSilicon.java | 2 +- .../util/DisabledOnAppleSiliconCondition.java | 2 +- .../src/test/resources/logging.properties | 0 .../src/test/resources/recording.jfr | Bin settings.gradle.kts | 2 +- 58 files changed, 94 insertions(+), 95 deletions(-) rename {inferred-spans-otel => inferred-spans}/build.gradle.kts (100%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/CallTree.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/InferredSpansConfiguration.java (97%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/InferredSpansProcessor.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/InferredSpansProcessorBuilder.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/ProfilingActivationListener.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/SamplingProfiler.java (99%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/SpanAnchoredClock.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/StackFrame.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/ThreadMatcher.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/TraceContext.java (96%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/asyncprofiler/AsyncProfiler.java (99%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/asyncprofiler/BufferedFile.java (99%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/asyncprofiler/JfrParser.java (97%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/asyncprofiler/ResourceExtractionUtil.java (99%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/CollectionUtil.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/Hashing.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/Int2IntHashMap.java (99%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/Int2ObjectHashMap.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/IntIntConsumer.java (97%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/Long2LongHashMap.java (99%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/Long2ObjectHashMap.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/LongHashSet.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/LongList.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/LongLongConsumer.java (97%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/collections/package-info.java (95%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/config/WildcardMatcher.java (99%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/pooling/AbstractObjectPool.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/pooling/Allocator.java (95%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/pooling/ObjectPool.java (97%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/pooling/QueueBasedObjectPool.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/pooling/Recyclable.java (95%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/pooling/Resetter.java (97%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/util/ByteUtils.java (97%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/util/HexUtils.java (98%) rename {inferred-spans-otel/src/main/java/co/elastic/apm => inferred-spans/src/main/java/co/elastic}/otel/profiler/util/ThreadUtils.java (97%) rename {inferred-spans-otel => inferred-spans}/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so (100%) rename {inferred-spans-otel => inferred-spans}/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so (100%) rename {inferred-spans-otel => inferred-spans}/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so (100%) rename {inferred-spans-otel => inferred-spans}/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so (100%) rename {inferred-spans-otel => inferred-spans}/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so (100%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/CallTreeSpanifyTest.java (97%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/CallTreeTest.java (99%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/FixedClock.java (97%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/ProfilerTestSetup.java (98%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/SamplingProfilerQueueTest.java (95%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/SamplingProfilerReplay.java (98%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/SamplingProfilerTest.java (99%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/ThreadMatcherTest.java (97%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/asyncprofiler/AsyncProfilerTest.java (91%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java (98%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/asyncprofiler/JfrParserTest.java (92%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/util/DisabledOnAppleSilicon.java (96%) rename {inferred-spans-otel/src/test/java/co/elastic/apm => inferred-spans/src/test/java/co/elastic}/otel/profiler/util/DisabledOnAppleSiliconCondition.java (97%) rename {inferred-spans-otel => inferred-spans}/src/test/resources/logging.properties (100%) rename {inferred-spans-otel => inferred-spans}/src/test/resources/recording.jfr (100%) diff --git a/build.gradle b/build.gradle index 9891d7486..c845cdbc5 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ subprojects { licenseHeaderFile(rootProject.file("buildscripts/spotless.reallogic.license.java"), "(package|import|public)") .named("reallogic") - .onlyIfContentMatches("package co.elastic.apm.otel.profiler.collections;") + .onlyIfContentMatches("package co.elastic.otel.profiler.collections;") } } diff --git a/inferred-spans-otel/build.gradle.kts b/inferred-spans/build.gradle.kts similarity index 100% rename from inferred-spans-otel/build.gradle.kts rename to inferred-spans/build.gradle.kts diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/CallTree.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/CallTree.java index 9144358b7..5e1164c1f 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/CallTree.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/CallTree.java @@ -16,16 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import static java.util.logging.Level.FINE; import static java.util.logging.Level.WARNING; -import co.elastic.apm.otel.profiler.collections.LongHashSet; -import co.elastic.apm.otel.profiler.collections.LongList; -import co.elastic.apm.otel.profiler.pooling.ObjectPool; -import co.elastic.apm.otel.profiler.pooling.Recyclable; -import co.elastic.apm.otel.profiler.util.HexUtils; +import co.elastic.otel.profiler.collections.LongHashSet; +import co.elastic.otel.profiler.collections.LongList; +import co.elastic.otel.profiler.pooling.ObjectPool; +import co.elastic.otel.profiler.pooling.Recyclable; +import co.elastic.otel.profiler.util.HexUtils; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansConfiguration.java similarity index 97% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansConfiguration.java index 73f9baee0..3577b8999 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansConfiguration.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansConfiguration.java @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; -import co.elastic.apm.otel.profiler.config.WildcardMatcher; +import co.elastic.otel.profiler.config.WildcardMatcher; import java.time.Duration; import java.util.List; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansProcessor.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansProcessor.java index 76d00f001..0f2150e2c 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessor.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansProcessor.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Tracer; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansProcessorBuilder.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansProcessorBuilder.java index f19e82624..51d1e7526 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/InferredSpansProcessorBuilder.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/InferredSpansProcessorBuilder.java @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; -import co.elastic.apm.otel.profiler.config.WildcardMatcher; +import co.elastic.otel.profiler.config.WildcardMatcher; import java.io.File; import java.time.Duration; import java.util.Arrays; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/ProfilingActivationListener.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/ProfilingActivationListener.java index 953a41e28..570d61004 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ProfilingActivationListener.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/ProfilingActivationListener.java @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; -import co.elastic.apm.otel.profiler.util.ThreadUtils; +import co.elastic.otel.profiler.util.ThreadUtils; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextStorage; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/SamplingProfiler.java similarity index 99% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/SamplingProfiler.java index 441a3a2d1..a77c2fd87 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SamplingProfiler.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/SamplingProfiler.java @@ -16,17 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import static java.nio.file.StandardOpenOption.READ; import static java.nio.file.StandardOpenOption.WRITE; -import co.elastic.apm.otel.profiler.asyncprofiler.AsyncProfiler; -import co.elastic.apm.otel.profiler.asyncprofiler.JfrParser; -import co.elastic.apm.otel.profiler.collections.Long2ObjectHashMap; -import co.elastic.apm.otel.profiler.config.WildcardMatcher; -import co.elastic.apm.otel.profiler.pooling.Allocator; -import co.elastic.apm.otel.profiler.pooling.ObjectPool; +import co.elastic.otel.profiler.asyncprofiler.AsyncProfiler; +import co.elastic.otel.profiler.asyncprofiler.JfrParser; +import co.elastic.otel.profiler.collections.Long2ObjectHashMap; +import co.elastic.otel.profiler.config.WildcardMatcher; +import co.elastic.otel.profiler.pooling.Allocator; +import co.elastic.otel.profiler.pooling.ObjectPool; import com.lmax.disruptor.EventFactory; import com.lmax.disruptor.EventPoller; import com.lmax.disruptor.EventTranslatorTwoArg; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/SpanAnchoredClock.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/SpanAnchoredClock.java index ebbea7bb8..1cb2a416d 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/SpanAnchoredClock.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/SpanAnchoredClock.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap; import io.opentelemetry.api.trace.Span; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/StackFrame.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/StackFrame.java index 8726c898c..c58c40c98 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/StackFrame.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/StackFrame.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import java.util.Objects; import javax.annotation.Nullable; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/ThreadMatcher.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/ThreadMatcher.java index c6bbc8bf0..9b1e1dff1 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/ThreadMatcher.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/ThreadMatcher.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; public class ThreadMatcher { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/TraceContext.java similarity index 96% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/TraceContext.java index 26e7d13ff..d3a6c12ee 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/TraceContext.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/TraceContext.java @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; -import co.elastic.apm.otel.profiler.pooling.Recyclable; -import co.elastic.apm.otel.profiler.util.ByteUtils; -import co.elastic.apm.otel.profiler.util.HexUtils; +import co.elastic.otel.profiler.pooling.Recyclable; +import co.elastic.otel.profiler.util.ByteUtils; +import co.elastic.otel.profiler.util.HexUtils; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfiler.java similarity index 99% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfiler.java index 3d87f9411..0be4ac356 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfiler.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfiler.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.asyncprofiler; +package co.elastic.otel.profiler.asyncprofiler; import java.io.IOException; import java.nio.file.Path; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/BufferedFile.java similarity index 99% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/BufferedFile.java index 17ea69edb..688c2e57a 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/BufferedFile.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/BufferedFile.java @@ -16,9 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.asyncprofiler; +package co.elastic.otel.profiler.asyncprofiler; -import co.elastic.apm.otel.profiler.pooling.Recyclable; +import co.elastic.otel.profiler.pooling.Recyclable; import java.io.File; import java.io.IOException; import java.nio.Buffer; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/JfrParser.java similarity index 97% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/JfrParser.java index e3c7ba710..19247c0ee 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParser.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/JfrParser.java @@ -16,15 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.asyncprofiler; +package co.elastic.otel.profiler.asyncprofiler; -import co.elastic.apm.otel.profiler.StackFrame; -import co.elastic.apm.otel.profiler.collections.Int2IntHashMap; -import co.elastic.apm.otel.profiler.collections.Int2ObjectHashMap; -import co.elastic.apm.otel.profiler.collections.Long2LongHashMap; -import co.elastic.apm.otel.profiler.collections.Long2ObjectHashMap; -import co.elastic.apm.otel.profiler.config.WildcardMatcher; -import co.elastic.apm.otel.profiler.pooling.Recyclable; +import co.elastic.otel.profiler.StackFrame; +import co.elastic.otel.profiler.collections.Int2IntHashMap; +import co.elastic.otel.profiler.collections.Int2ObjectHashMap; +import co.elastic.otel.profiler.collections.Long2LongHashMap; +import co.elastic.otel.profiler.collections.Long2ObjectHashMap; +import co.elastic.otel.profiler.config.WildcardMatcher; +import co.elastic.otel.profiler.pooling.Recyclable; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/ResourceExtractionUtil.java similarity index 99% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/ResourceExtractionUtil.java index 9a845ec17..97024407b 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/asyncprofiler/ResourceExtractionUtil.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/asyncprofiler/ResourceExtractionUtil.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.asyncprofiler; +package co.elastic.otel.profiler.asyncprofiler; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; import static java.nio.file.StandardOpenOption.CREATE_NEW; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/CollectionUtil.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/CollectionUtil.java index 2464a5ff6..63276fb22 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/CollectionUtil.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/CollectionUtil.java @@ -31,7 +31,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; /** Utility functions for collection objects. */ public class CollectionUtil { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Hashing.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Hashing.java index a8343a5ac..024af2c95 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Hashing.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Hashing.java @@ -31,7 +31,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; /** Hashing functions for applying to integers. */ public class Hashing { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2IntHashMap.java similarity index 99% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2IntHashMap.java index 387121d1b..8f94cece7 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2IntHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2IntHashMap.java @@ -31,7 +31,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; import java.io.Serializable; import java.util.AbstractCollection; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2ObjectHashMap.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2ObjectHashMap.java index db5361883..06adcae75 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Int2ObjectHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2ObjectHashMap.java @@ -31,10 +31,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; +import static co.elastic.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.otel.profiler.collections.CollectionUtil.validateLoadFactor; import static java.util.Objects.requireNonNull; import java.io.Serializable; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/IntIntConsumer.java similarity index 97% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/IntIntConsumer.java index 2683217b5..7e370885f 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/IntIntConsumer.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/IntIntConsumer.java @@ -31,7 +31,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; /** This is an (int, int) primitive specialisation of a BiConsumer */ @FunctionalInterface diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2LongHashMap.java similarity index 99% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2LongHashMap.java index a3a083a1b..c5e03b2c6 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2LongHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2LongHashMap.java @@ -31,10 +31,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; +import static co.elastic.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.otel.profiler.collections.CollectionUtil.validateLoadFactor; import java.io.Serializable; import java.util.AbstractCollection; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2ObjectHashMap.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2ObjectHashMap.java index e7f6de552..dd599df0a 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/Long2ObjectHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2ObjectHashMap.java @@ -31,10 +31,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; +import static co.elastic.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.otel.profiler.collections.CollectionUtil.validateLoadFactor; import static java.util.Objects.requireNonNull; import java.io.Serializable; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongHashSet.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongHashSet.java index 55850370e..5aa9b5954 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongHashSet.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongHashSet.java @@ -31,10 +31,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; -import static co.elastic.apm.otel.profiler.collections.CollectionUtil.validateLoadFactor; +import static co.elastic.otel.profiler.collections.CollectionUtil.findNextPositivePowerOfTwo; +import static co.elastic.otel.profiler.collections.CollectionUtil.validateLoadFactor; import java.io.Serializable; import java.lang.reflect.Array; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongList.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongList.java index 7f90a649f..05a08237a 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongList.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongList.java @@ -31,7 +31,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; import java.util.Arrays; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongLongConsumer.java similarity index 97% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongLongConsumer.java index 66c85c850..2b959cb0a 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/LongLongConsumer.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongLongConsumer.java @@ -31,7 +31,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; /** This is an (long, long) primitive specialisation of a BiConsumer */ @FunctionalInterface diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/package-info.java similarity index 95% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/collections/package-info.java index 4f7ac61bb..5df9446a8 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/collections/package-info.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/package-info.java @@ -25,4 +25,4 @@ * Java 7. That's why the relevant classes are copied over and methods referencing Java 8 types are * removed. */ -package co.elastic.apm.otel.profiler.collections; +package co.elastic.otel.profiler.collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/config/WildcardMatcher.java similarity index 99% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/config/WildcardMatcher.java index b2ad902a5..a024bf74f 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/config/WildcardMatcher.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/config/WildcardMatcher.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.config; +package co.elastic.otel.profiler.config; import java.util.ArrayList; import java.util.Collections; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/AbstractObjectPool.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/AbstractObjectPool.java index 31af5cee7..b39914b27 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/AbstractObjectPool.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/AbstractObjectPool.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.pooling; +package co.elastic.otel.profiler.pooling; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Allocator.java similarity index 95% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Allocator.java index 194a9f8fa..b9ec01b09 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Allocator.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Allocator.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.pooling; +package co.elastic.otel.profiler.pooling; /** * Defines pooled object factory diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/ObjectPool.java similarity index 97% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/ObjectPool.java index 81455c660..ceea2c960 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/ObjectPool.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/ObjectPool.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.pooling; +package co.elastic.otel.profiler.pooling; import org.jctools.queues.MpmcArrayQueue; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/QueueBasedObjectPool.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/QueueBasedObjectPool.java index fadfbe223..a5d9b1410 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/QueueBasedObjectPool.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/QueueBasedObjectPool.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.pooling; +package co.elastic.otel.profiler.pooling; import java.util.Queue; import javax.annotation.Nullable; diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Recyclable.java similarity index 95% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Recyclable.java index 26ce10fce..8743ef0be 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Recyclable.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Recyclable.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.pooling; +package co.elastic.otel.profiler.pooling; public interface Recyclable { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Resetter.java similarity index 97% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Resetter.java index f68e7f088..c3088c25a 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/pooling/Resetter.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/pooling/Resetter.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.pooling; +package co.elastic.otel.profiler.pooling; /** * Defines reset strategy to use for a given pooled object type when they are returned to pool diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/util/ByteUtils.java similarity index 97% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/util/ByteUtils.java index 853ae9dd9..b3e7b939a 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ByteUtils.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/util/ByteUtils.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.util; +package co.elastic.otel.profiler.util; public class ByteUtils { public static void putLong(byte[] buffer, int offset, long l) { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/util/HexUtils.java similarity index 98% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/util/HexUtils.java index f6a30d3de..b69b55278 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/HexUtils.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/util/HexUtils.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.util; +package co.elastic.otel.profiler.util; public class HexUtils { diff --git a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/util/ThreadUtils.java similarity index 97% rename from inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java rename to inferred-spans/src/main/java/co/elastic/otel/profiler/util/ThreadUtils.java index e8acc3ffe..ba36ec5f5 100644 --- a/inferred-spans-otel/src/main/java/co/elastic/apm/otel/profiler/util/ThreadUtils.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/util/ThreadUtils.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.util; +package co.elastic.otel.profiler.util; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; diff --git a/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so b/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so similarity index 100% rename from inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so rename to inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-aarch64.so diff --git a/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so b/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so similarity index 100% rename from inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so rename to inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-arm.so diff --git a/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so b/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so similarity index 100% rename from inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so rename to inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x64.so diff --git a/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so b/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so similarity index 100% rename from inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so rename to inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-linux-x86.so diff --git a/inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so b/inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so similarity index 100% rename from inferred-spans-otel/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so rename to inferred-spans/src/main/resources/asyncprofiler/libasyncProfiler-macos-x64.so diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/CallTreeSpanifyTest.java similarity index 97% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/CallTreeSpanifyTest.java index 42f23c9fd..1fda650cd 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeSpanifyTest.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/CallTreeSpanifyTest.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import co.elastic.apm.otel.profiler.pooling.ObjectPool; -import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import co.elastic.otel.profiler.pooling.ObjectPool; +import co.elastic.otel.profiler.util.DisabledOnAppleSilicon; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/CallTreeTest.java similarity index 99% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/CallTreeTest.java index 496fbf0ab..1adbc3eb4 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/CallTreeTest.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/CallTreeTest.java @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static java.util.stream.Collectors.toMap; -import co.elastic.apm.otel.profiler.pooling.ObjectPool; -import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import co.elastic.otel.profiler.pooling.ObjectPool; +import co.elastic.otel.profiler.util.DisabledOnAppleSilicon; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.Tracer; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedClock.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/FixedClock.java similarity index 97% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedClock.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/FixedClock.java index 68c6970ba..e83fabf28 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/FixedClock.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/FixedClock.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/ProfilerTestSetup.java similarity index 98% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/ProfilerTestSetup.java index a2d352c20..80f66511e 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ProfilerTestSetup.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/ProfilerTestSetup.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerQueueTest.java similarity index 95% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerQueueTest.java index a3a231397..46a757ad4 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerQueueTest.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerQueueTest.java @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import static org.assertj.core.api.Assertions.assertThat; -import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import co.elastic.otel.profiler.util.DisabledOnAppleSilicon; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerReplay.java similarity index 98% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerReplay.java index e0f73d6d2..a75dda0f5 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerReplay.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerReplay.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import java.io.File; import java.nio.file.Files; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerTest.java similarity index 99% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerTest.java index e2d45d437..574587cd8 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/SamplingProfilerTest.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/SamplingProfilerTest.java @@ -16,14 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; -import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import co.elastic.otel.profiler.util.DisabledOnAppleSilicon; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/ThreadMatcherTest.java similarity index 97% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/ThreadMatcherTest.java index 8cbc77266..082731d86 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/ThreadMatcherTest.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/ThreadMatcherTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler; +package co.elastic.otel.profiler; import static org.assertj.core.api.Assertions.assertThat; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerTest.java similarity index 91% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerTest.java index 96c07e3f6..85a4b466e 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerTest.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerTest.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.asyncprofiler; +package co.elastic.otel.profiler.asyncprofiler; -import static co.elastic.apm.otel.profiler.asyncprofiler.AsyncProfiler.SAFEMODE_SYSTEM_PROPERTY_NAME; +import static co.elastic.otel.profiler.asyncprofiler.AsyncProfiler.SAFEMODE_SYSTEM_PROPERTY_NAME; import static org.assertj.core.api.Assertions.assertThat; -import co.elastic.apm.otel.profiler.util.DisabledOnAppleSilicon; +import co.elastic.otel.profiler.util.DisabledOnAppleSilicon; import java.io.File; import java.io.FilenameFilter; import org.junit.jupiter.api.BeforeEach; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java similarity index 98% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java index 988ffe454..97babf4ea 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.asyncprofiler; +package co.elastic.otel.profiler.asyncprofiler; import static org.assertj.core.api.Assertions.assertThat; @@ -82,8 +82,7 @@ void updateAsyncProfilerBinaries() throws Exception { Path thisOsLib = getBinariesResourceDir() .resolve( - co.elastic.apm.otel.profiler.asyncprofiler.AsyncProfiler.getLibraryFileName() - + ".so"); + co.elastic.otel.profiler.asyncprofiler.AsyncProfiler.getLibraryFileName() + ".so"); AsyncProfiler asyncProfiler = AsyncProfiler.getInstance(thisOsLib.toString()); assertThat(asyncProfiler.getVersion()).isEqualTo(TARGET_VERSION); } diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/JfrParserTest.java similarity index 92% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/JfrParserTest.java index 24f60c223..60ef4210c 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/asyncprofiler/JfrParserTest.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/JfrParserTest.java @@ -16,12 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.asyncprofiler; +package co.elastic.otel.profiler.asyncprofiler; -import static co.elastic.apm.otel.profiler.config.WildcardMatcher.caseSensitiveMatcher; +import static co.elastic.otel.profiler.config.WildcardMatcher.caseSensitiveMatcher; import static org.assertj.core.api.Assertions.assertThat; -import co.elastic.apm.otel.profiler.StackFrame; +import co.elastic.otel.profiler.StackFrame; import java.io.File; import java.nio.ByteBuffer; import java.nio.file.Paths; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/util/DisabledOnAppleSilicon.java similarity index 96% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/util/DisabledOnAppleSilicon.java index c28210e74..e0642fb04 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSilicon.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/util/DisabledOnAppleSilicon.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.util; +package co.elastic.otel.profiler.util; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/util/DisabledOnAppleSiliconCondition.java similarity index 97% rename from inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java rename to inferred-spans/src/test/java/co/elastic/otel/profiler/util/DisabledOnAppleSiliconCondition.java index 1381b71f9..f85f65fc7 100644 --- a/inferred-spans-otel/src/test/java/co/elastic/apm/otel/profiler/util/DisabledOnAppleSiliconCondition.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/util/DisabledOnAppleSiliconCondition.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.apm.otel.profiler.util; +package co.elastic.otel.profiler.util; import static org.junit.jupiter.api.extension.ConditionEvaluationResult.disabled; import static org.junit.jupiter.api.extension.ConditionEvaluationResult.enabled; diff --git a/inferred-spans-otel/src/test/resources/logging.properties b/inferred-spans/src/test/resources/logging.properties similarity index 100% rename from inferred-spans-otel/src/test/resources/logging.properties rename to inferred-spans/src/test/resources/logging.properties diff --git a/inferred-spans-otel/src/test/resources/recording.jfr b/inferred-spans/src/test/resources/recording.jfr similarity index 100% rename from inferred-spans-otel/src/test/resources/recording.jfr rename to inferred-spans/src/test/resources/recording.jfr diff --git a/settings.gradle.kts b/settings.gradle.kts index befc31ec2..74dea2401 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,7 +14,7 @@ include("agent") include("bootstrap") include("custom") include("instrumentation") -include("inferred-spans-otel") +include("inferred-spans") include("resources") include("resources:repackaged") include("smoke-tests") From e744afb9e84f8d0b909da024f30d301b5c90aea4 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Tue, 9 Jan 2024 12:08:19 +0100 Subject: [PATCH 15/18] Update gradle/libs.versions.toml Co-authored-by: SylvainJuge <763082+SylvainJuge@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a45d778e..01a5a8551 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ opentelemetryInstrumentationAlphaBom = { group = "io.opentelemetry.instrumentati awsContribResources = { group = "io.opentelemetry.contrib", name = "opentelemetry-aws-resources", version.ref = "opentelemetryContribAlpha" } contribResources = { group = "io.opentelemetry.contrib", name = "opentelemetry-resource-providers", version.ref = "opentelemetryContribAlpha" } -assertJ-core = "org.assertj:assertj-core:3.24.2" +assertj-core = "org.assertj:assertj-core:3.24.2" awaitility = "org.awaitility:awaitility:4.2.0" findbugs-jsr305 = "com.google.code.findbugs:jsr305:3.0.0" From 68bff2d6d5efddc34fbe9e0bd466a32ccf09c9f2 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Tue, 9 Jan 2024 12:25:17 +0100 Subject: [PATCH 16/18] Review fixes --- buildscripts/spotless.reallogic.license.java | 2 +- inferred-spans/build.gradle.kts | 2 +- .../otel/profiler/collections/CollectionUtil.java | 2 +- .../elastic/otel/profiler/collections/Hashing.java | 2 +- .../otel/profiler/collections/Int2IntHashMap.java | 2 +- .../profiler/collections/Int2ObjectHashMap.java | 2 +- .../otel/profiler/collections/IntIntConsumer.java | 2 +- .../otel/profiler/collections/Long2LongHashMap.java | 2 +- .../profiler/collections/Long2ObjectHashMap.java | 2 +- .../otel/profiler/collections/LongHashSet.java | 2 +- .../elastic/otel/profiler/collections/LongList.java | 2 +- .../otel/profiler/collections/LongLongConsumer.java | 2 +- .../asyncprofiler/AsyncProfilerUpgrader.java | 13 +++++++------ 13 files changed, 19 insertions(+), 18 deletions(-) diff --git a/buildscripts/spotless.reallogic.license.java b/buildscripts/spotless.reallogic.license.java index 6ef2949a3..bd5fc86f4 100644 --- a/buildscripts/spotless.reallogic.license.java +++ b/buildscripts/spotless.reallogic.license.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/build.gradle.kts b/inferred-spans/build.gradle.kts index 9018d6533..40c631ad2 100644 --- a/inferred-spans/build.gradle.kts +++ b/inferred-spans/build.gradle.kts @@ -12,7 +12,7 @@ dependencies { testCompileOnly(libs.findbugs.jsr305) testImplementation("io.opentelemetry:opentelemetry-sdk") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") - testImplementation(libs.assertJ.core) + testImplementation(libs.assertj.core) testImplementation(libs.awaitility) testImplementation("org.kohsuke:github-api:1.133") testImplementation("org.apache.commons:commons-compress:1.21") diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/CollectionUtil.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/CollectionUtil.java index 63276fb22..e28a5794c 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/CollectionUtil.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/CollectionUtil.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Hashing.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Hashing.java index 024af2c95..65fd9df55 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Hashing.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Hashing.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2IntHashMap.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2IntHashMap.java index 8f94cece7..5014d7721 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2IntHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2IntHashMap.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2ObjectHashMap.java index 06adcae75..7749ee904 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2ObjectHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Int2ObjectHashMap.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/IntIntConsumer.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/IntIntConsumer.java index 7e370885f..3c0e6ec7b 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/IntIntConsumer.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/IntIntConsumer.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2LongHashMap.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2LongHashMap.java index c5e03b2c6..adddb09ff 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2LongHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2LongHashMap.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2ObjectHashMap.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2ObjectHashMap.java index dd599df0a..a5b84eb49 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2ObjectHashMap.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/Long2ObjectHashMap.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongHashSet.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongHashSet.java index 5aa9b5954..8089d5503 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongHashSet.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongHashSet.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongList.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongList.java index 05a08237a..f5f0b1585 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongList.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongList.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongLongConsumer.java b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongLongConsumer.java index 2b959cb0a..d044e1dfa 100644 --- a/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongLongConsumer.java +++ b/inferred-spans/src/main/java/co/elastic/otel/profiler/collections/LongLongConsumer.java @@ -17,7 +17,7 @@ * under the License. */ /* - * Copyright 2014-2019 Real Logic Ltd. + * Copyright 2014-2020 Real Logic Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java index 97babf4ea..44c82e987 100644 --- a/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java @@ -57,7 +57,7 @@ public class AsyncProfilerUpgrader { static final String COMMON_BINARY_FILE_NAME = "libasyncProfiler.so"; static final String[] USED_ARTIFACTS = { - "linux-aarch64", "linux-arm", "linux-x64", "linux-x86", "macos-x64" + "linux-aarch64", "linux-arm", "linux-x64", "linux-x86", "macos-x64" }; @Test @@ -207,14 +207,15 @@ private void replaceBinary(Path downloadedArtifact, String artifactName) throws } private Path getBinariesResourceDir() throws URISyntaxException { - // /apm-agent-plugins/apm-profiling-plugin/target/classes/asyncprofiler + // /inferred-spans/build/resources/main/asyncprofiler Path asyncProfilerTestResourcePath = Paths.get(AsyncProfilerUpgrader.class.getResource("/asyncprofiler").toURI()); - // /apm-agent-plugins/apm-profiling-plugin/target/classes/asyncprofiler - Path pluginRootDir = asyncProfilerTestResourcePath.getParent().getParent().getParent(); + // /inferred-spans/build/resources/main/asyncprofiler + Path projectRootDir = + asyncProfilerTestResourcePath.getParent().getParent().getParent().getParent(); // We are looking for // - // /apm-agent-plugins/apm-profiling-plugin/src/main/resources/asyncprofiler - return pluginRootDir + // </inferred-spans/src/main/resources/asyncprofiler + return projectRootDir .resolve("src") .resolve("main") .resolve("resources") From 1689bc8d0f650a3687d245f4fa55a5a14d9389c9 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Tue, 9 Jan 2024 12:26:43 +0100 Subject: [PATCH 17/18] Fix conflicts --- jvmti-access/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jvmti-access/build.gradle.kts b/jvmti-access/build.gradle.kts index 49479e287..fcc19ab24 100644 --- a/jvmti-access/build.gradle.kts +++ b/jvmti-access/build.gradle.kts @@ -13,7 +13,7 @@ plugins { } dependencies { - testImplementation(libs.assertJ.core) + testImplementation(libs.assertj.core) } // we use Java 7 for this project so that it can be reused in the old elastic-apm-agent From 641755983fc5eb3c770ca71a420d1346744a4a21 Mon Sep 17 00:00:00 2001 From: Jonas Kunz Date: Tue, 9 Jan 2024 12:30:08 +0100 Subject: [PATCH 18/18] spotless --- .../otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java index 44c82e987..599f39ff5 100644 --- a/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java +++ b/inferred-spans/src/test/java/co/elastic/otel/profiler/asyncprofiler/AsyncProfilerUpgrader.java @@ -57,7 +57,7 @@ public class AsyncProfilerUpgrader { static final String COMMON_BINARY_FILE_NAME = "libasyncProfiler.so"; static final String[] USED_ARTIFACTS = { - "linux-aarch64", "linux-arm", "linux-x64", "linux-x86", "macos-x64" + "linux-aarch64", "linux-arm", "linux-x64", "linux-x86", "macos-x64" }; @Test