You are viewing a plain text version of this content. The canonical link for it is here.
Posted to pr@cassandra.apache.org by "netudima (via GitHub)" <gi...@apache.org> on 2025/08/19 22:48:38 UTC

[PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

netudima opened a new pull request, #4324:
URL: https://github.com/apache/cassandra/pull/4324

   Optimize Counter, Meter and Histogram metrics using thread local counters
   
   Codahale metrics do not provide the ability to create custom metric implementations, so we have to inherit from Codahale classes. For better cache locality rate and counter values are extracted to a common thread-local arrays. Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects. Phantom references are used to track aliveness of metric users and reusing of metric IDs.
   
   Patch by Dmitry Konstantinov; reviewed by TBD for CASSANDRA-20250
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319100234


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();

Review Comment:
   yes, it is for this purpose, I have a comment about it near summaryLock declaration
   it is good to add a clarification here as well



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318408276


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);

Review Comment:
   Should we consider holding onto a reference to the cleaner object, so that we can clean it up ourselves if we don't need to rely on GC reachability, as we expect to?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316660380


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long summaryLocal;
+        long result;
+        AtomicLong summary = getSummary(metricId);
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            summaryLocal = summary.get();
+            result = 0;
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                if (threadLocalMetrics != null)
+                {
+                    long count = 0;
+                    long[] currentCounterValues = threadLocalMetrics.counterValues;
+                    // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                    if (metricId < currentCounterValues.length)
+                        count = currentCounterValues[metricId];
+                    result += count;
+                }
+            }
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        result += summaryLocal;
+        if (resetToZero)
+            summary.addAndGet(-result); // compensative reset without writing to thread local values
+        return result;
+    }
+
+    private static AtomicLong getSummary(int metricId)
+    {
+        AtomicLong result = summaryValues.get(metricId);
+        if (result != null)
+            return result;
+        return summaryValues.computeIfAbsent(metricId, (metricIdToAdd) -> new AtomicLong());
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[(int)(metricId * 1.1)];

Review Comment:
   Math.max(metricId+1, metricId * 1.1)?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316172511


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter

Review Comment:
   If I got the idea correctly it is already present. rates array is not per-thread and updated periodically by a background thread. We do not use separate counters per rate/EWMA, I've merged them together, so have only 2 thread local counters: countMetricId and uncountedMetricId in total. uncountedMetricId is used in the way you mentioned and countMetricId is needed because we need to provide total count as well for Meter objects.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2340128171


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   I think we have a couple of race conditions here. The main one is when the threadLocalMetric resizes itself, which is done in without coordination wrt this operation, and may allow this reset to be lost. The less serious one is that there's no obvious happens-before relationship between this write and the read of the entry by the thread when it sees the metric id reused. Perhaps we can simply rely on the safe publication of the metric itself to that thread; this one doesn't overly worry me, anyway. The main problem can be addressed a few ways, the simplest being to synchronise on the instance object we're updating, and also when resizing.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2358225281


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   We could also offset the cost by extending `FastThreadLocalThread` and making our `ThreadLocalMetrics` instance an instance variable, since this is accessed incredibly often and is essentially its own collection of thread local metrics. This would anyway be a good thing to do, but could be done in a follow-up patch.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2358222199


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   There is one other option I have thought of, that also has an ongoing cost but it should be uniform between systems so we can at least measure it, and it's very simple:
   
   Add a global volatile counter we update whenever we zero and reissue a recycled metric id, and assign the current value of the counter to a final field in the Metric. Inside each instance of ThreadLocalMetrics, track the highest counter we've seen. On read compare the metric counter to the ThreadLocalMetrics counter, and if higher read the global volatile counter and assign the result to our instance counter. This provides a proper happens-before relationship, at the cost of two extra loads (but from adjacent memory) and one extra comparison. We could perhaps reduce the cost further by storing the metric id in a long, and using the top e.g. 40 bits for the counter (and use the same top bits in the volatile counter, so we can compare without transforming), so that we pay only one extra load, one comparison, and one bit mask operation.
   
   The compensative approach should be fine though, and has no ongoing costs, and we have no reason to doubt its correctness so perhaps it should be preferred. But it would be nice to have a way to actually zero the thread local counters reliably.
   
   I am happy for you to decide which approach you prefer to pursue, or if you want to experiment a bit (or not).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319350197


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];
+                System.arraycopy(rates, 0, newRates, 0, rates.length);
+                rates = newRates;
+            }
+            rates[rateGroupId +  M1_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId +  M5_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId + M15_RATE_OFFSET] = NON_INITIALIZED;
+        }
+        return rateGroupId;
+    }
+
+    static double getRateValue(int offset)
+    {
+        return rates[offset];
+    }
+
+    private static void setRateValue(int offset, double value)
+    {
+        rates[offset] = value;
+    }
+
+    private final int countMetricId;
+    private final int uncountedMetricId;
+    private final int rateGroupId;
+    private final long startTime;
+    private final MonotonicClock clock;
+    private long lastTick;
+
+    public ThreadLocalMeter()
+    {
+        this(MonotonicClock.Global.approxTime);
+    }
+
+    public ThreadLocalMeter(MonotonicClock clock)
+    {
+        // movingAverages is set to null to reduce metrics memory footprint
+        super(null, Clock.defaultClock());
+        this.clock = clock;
+        this.startTime = this.clock.now();
+        this.lastTick = this.startTime;
+        this.countMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.uncountedMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.rateGroupId = allocateRateGroupOffset();
+        allMeters.add(new WeakReference<>(this));
+        ThreadLocalMetrics.destroyWhenUnreachable(this, new MeterCleaner(countMetricId, uncountedMetricId, rateGroupId));
+        ReflectionUtils.setFieldToNull(this, "count"); // to reduce metrics memory footprint
+    }
+
+    private static class MeterCleaner implements ThreadLocalMetrics.MetricCleaner
+    {
+        private final int countMetricId;
+        private final int uncountedMetricId;
+        private final int rateGroupId;
+
+        private MeterCleaner(int countMetricId, int uncountedMetricId, int rateGroupId)
+        {
+            this.countMetricId = countMetricId;
+            this.uncountedMetricId = uncountedMetricId;
+            this.rateGroupId = rateGroupId;
+        }
+
+        @Override
+        public void clean()
+        {
+            recycleRateGroupId(rateGroupId);
+            ThreadLocalMetrics.recycleMetricId(countMetricId);
+            ThreadLocalMetrics.recycleMetricId(uncountedMetricId);
+        }
+
+        private static void recycleRateGroupId(int rateGroupId)
+        {
+            synchronized (freeRateGroupIdSetGuard)
+            {
+                freeRateGroupIdSet.set(rateGroupId);
+            }
+        }
+    }
+
+    /**
+     * Mark the occurrence of an event.
+     */
+    public void mark()
+    {
+        mark(1);
+    }
+
+    /**
+     * Mark the occurrence of a given number of events.
+     *
+     * @param n the number of events
+     */
+    public void mark(long n)
+    {
+        // we retrieve the context once here to reduce thread local lookup overheads
+        ThreadLocalMetrics context = ThreadLocalMetrics.get();
+        context.addNonStatic(countMetricId, n);
+        context.addNonStatic(uncountedMetricId, n);
+    }
+
+    @Override
+    public long getCount()
+    {
+        return ThreadLocalMetrics.getCount(countMetricId);
+    }
+
+    @Override
+    public double getFifteenMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M15_RATE_OFFSET));
+    }
+
+    @Override
+    public double getFiveMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M5_RATE_OFFSET));
+    }
+
+    @Override
+    public double getOneMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M1_RATE_OFFSET));
+    }
+
+    @Override
+    public double getMeanRate()
+    {
+        long count = getCount();
+        if (count == 0)
+            return 0.0;
+        else
+        {
+            final double elapsed = clock.now() - startTime;
+            return count / elapsed * TimeUnit.SECONDS.toNanos(1);

Review Comment:
   Ah, this isn’t important at all, so if this affects your testing negatively don’t worry



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2315533493


##########
src/java/org/apache/cassandra/metrics/AtomicLongCounter.java:
##########
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.cassandra.utils.ReflectionUtils;
+
+/**
+ * This type of Counter is not very efficient for updates but fast to read.

Review Comment:
   This is I think not an entirely correct comment. This is more efficient to update unless highly contended. `LongAdder` degrades its performance significantly as soon as it experiences any contention, after which we pay higher costs in perpetuity. I would generally prefer something like `AtomicLong` over `LongAdder` until demonstrated that a given counter is highly contended for writes and warrants the cost of a `LongAdder`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316621665


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];

Review Comment:
   why cast to int? should already be int?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3338389556

   I don't have a super strong opinion about this, except that `ShardedHistogram` should not extend `ThreadLocalHistogram`, as it is an abstraction for cases where there is already some other mutual exclusion / sharding capability. It's essentially an alternative to `ThreadLocalHistogram`. Absolutely on board with removing `CassandraHistogram` as well though, yes.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3338500237

   Yep, that looks good to me!


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319321011


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);

Review Comment:
   I thought I hadn’t posted this comment. I changed my mind in the process of writing it, deciding it wasn’t worthwhile.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318301793


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];
+                System.arraycopy(rates, 0, newRates, 0, rates.length);
+                rates = newRates;
+            }
+            rates[rateGroupId +  M1_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId +  M5_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId + M15_RATE_OFFSET] = NON_INITIALIZED;
+        }
+        return rateGroupId;
+    }
+
+    static double getRateValue(int offset)
+    {
+        return rates[offset];
+    }
+
+    private static void setRateValue(int offset, double value)
+    {
+        rates[offset] = value;
+    }
+
+    private final int countMetricId;
+    private final int uncountedMetricId;
+    private final int rateGroupId;
+    private final long startTime;
+    private final MonotonicClock clock;
+    private long lastTick;
+
+    public ThreadLocalMeter()
+    {
+        this(MonotonicClock.Global.approxTime);
+    }
+
+    public ThreadLocalMeter(MonotonicClock clock)
+    {
+        // movingAverages is set to null to reduce metrics memory footprint
+        super(null, Clock.defaultClock());
+        this.clock = clock;
+        this.startTime = this.clock.now();
+        this.lastTick = this.startTime;
+        this.countMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.uncountedMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.rateGroupId = allocateRateGroupOffset();
+        allMeters.add(new WeakReference<>(this));
+        ThreadLocalMetrics.destroyWhenUnreachable(this, new MeterCleaner(countMetricId, uncountedMetricId, rateGroupId));
+        ReflectionUtils.setFieldToNull(this, "count"); // to reduce metrics memory footprint
+    }
+
+    private static class MeterCleaner implements ThreadLocalMetrics.MetricCleaner
+    {
+        private final int countMetricId;
+        private final int uncountedMetricId;
+        private final int rateGroupId;
+
+        private MeterCleaner(int countMetricId, int uncountedMetricId, int rateGroupId)
+        {
+            this.countMetricId = countMetricId;
+            this.uncountedMetricId = uncountedMetricId;
+            this.rateGroupId = rateGroupId;
+        }
+
+        @Override
+        public void clean()
+        {
+            recycleRateGroupId(rateGroupId);
+            ThreadLocalMetrics.recycleMetricId(countMetricId);
+            ThreadLocalMetrics.recycleMetricId(uncountedMetricId);
+        }
+
+        private static void recycleRateGroupId(int rateGroupId)
+        {
+            synchronized (freeRateGroupIdSetGuard)
+            {
+                freeRateGroupIdSet.set(rateGroupId);
+            }
+        }
+    }
+
+    /**
+     * Mark the occurrence of an event.
+     */
+    public void mark()
+    {
+        mark(1);
+    }
+
+    /**
+     * Mark the occurrence of a given number of events.
+     *
+     * @param n the number of events
+     */
+    public void mark(long n)
+    {
+        // we retrieve the context once here to reduce thread local lookup overheads
+        ThreadLocalMetrics context = ThreadLocalMetrics.get();
+        context.addNonStatic(countMetricId, n);
+        context.addNonStatic(uncountedMetricId, n);
+    }
+
+    @Override
+    public long getCount()
+    {
+        return ThreadLocalMetrics.getCount(countMetricId);
+    }
+
+    @Override
+    public double getFifteenMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M15_RATE_OFFSET));
+    }
+
+    @Override
+    public double getFiveMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M5_RATE_OFFSET));
+    }
+
+    @Override
+    public double getOneMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M1_RATE_OFFSET));
+    }
+
+    @Override
+    public double getMeanRate()
+    {
+        long count = getCount();
+        if (count == 0)
+            return 0.0;
+        else
+        {
+            final double elapsed = clock.now() - startTime;
+            return count / elapsed * TimeUnit.SECONDS.toNanos(1);

Review Comment:
   This formulation is surprising to me. I assume it's to avoid additional divisions, but perhaps we can make `1d/TimeUnit.SECONDS.toNanos(1)` a static constant so we can convert elapsed to a floating point number of seconds by multiplying by it in the first step? Probably also leads to marginally better floating point precision (not that it matters here).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2317064396


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)

Review Comment:
   I afraid of 2 kinds of issues for this approach:
   
   1. the metric read will become an eventual consistent and different tests which use metrics to verify (I definitely saw such ones) will become flaky
   2. it may cause confusing artefacts for the reported metrics when an interval to collect is small enough (which is not always true but a realistic scenario IMHO) 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima closed pull request #4324: [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters
URL: https://github.com/apache/cassandra/pull/4324


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2370226608


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   > There is one other option I have thought of, that also has an ongoing cost but it should be uniform between systems so we can at least measure it, and it's very simple...
   
   Yes, it is exactly the same thing (in much more details) what I described on a high level as an option 2 :-)
   It requires some time to experiment and measure, so I suggest to do it as a part of the follow-up patch to not delay the current patch applying.
   
   > We could also offset the cost by extending FastThreadLocalThread and making our ThreadLocalMetrics instance an instance variable
   
   yes, it is really a good idea for the follow-up patch! Probably a tracing state also can be a candidate for such optimisation...
   
   So, in total there are the following things for the follow-up patch:
   - to measure overheads for reads and evaluate a batch snapshotting to reduce read costs
   - I am thinking about trying StampedLock instead of ReadWriteLock
   - Extending FastThreadLocalThread and making ThreadLocalMetrics instance an instance variable
   - explore potential options to reduce memory usage such as using of int to int maps or other more compact representations



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318396137


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();

Review Comment:
   I assume the writeLock is taken here to avoid races with readers that might double count this thread's data otherwise. Might be worth commenting to this effect.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316640440


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))

Review Comment:
   should have braces for this multi-line if



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316649659


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)

Review Comment:
   Should we consider a batch version of this where we compute a snapshot of the summary+thread local for all metrics, and refresh this if older than some number of seconds and otherwise serve from the snapshot?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316641545


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long summaryLocal;
+        long result;
+        AtomicLong summary = getSummary(metricId);
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            summaryLocal = summary.get();
+            result = 0;
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                if (threadLocalMetrics != null)

Review Comment:
   can this be null?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318263434


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)

Review Comment:
   So, for context this is the approach I have taken with the new Accord metrics, to limit the number of times we take the lock used for processing transactions. Here the primary reason would be to make JMX polling agents much lower overhead. Of course, we want some way to force a synchronous update or read, and we probably want the default maximum staleness to be configurable, but I think this is then sufficient to handle your concerns.
   
   That said, it's absolutely just a suggestion. I think it's fine as is, and if you prefer we can wait to see if JMX metric collectors show up more in traces before we explore this capability further.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3338190547

   I see, I've missed that you've already overridden getCount logic in some branches of the hierarchy. The current trunk state:
   ```mermaid
   graph TD;
       CassandraHistogram --> com.codahale.metrics.Histogram
       ClearableHistogram --> CassandraHistogram;
       OverrideHistogram --> CassandraHistogram;
       ShardedHistogram --> OverrideHistogram;
       OnDemandHistogram --> OverrideHistogram;
   ```
   
   Did I get correctly what you suggest the following hierarchy? (where `Histogram` interface is merged into `CassandraHistogram` class and not present anymore as a separate interface)
   ```mermaid
   graph TD;
       CassandraHistogram --> com.codahale.metrics.Histogram;
       ThreadLocalHistogram --> OverrideHistogram;
       ClearableHistogram --> ThreadLocalHistogram;
       OverrideHistogram --> CassandraHistogram;
       ShardedHistogram --> ThreadLocalHistogram;
       OnDemandHistogram --> OverrideHistogram;
   ```


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2353654009


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   Thank you for highlighting the issue! I kept the extension case in my mind when wrote getCount logic but forgot about the recycling. Yes, it looks like adding a synchronization on ThreadLocalMetrics instance is a simplest option. 
   
   Regarding the safe publication, yes, it is tricky part. 
   We have a kind of happens-before path like: 
   thread 1: put id into freeMetricIdSet using synchronization
   thread 2: get id from freeMetricIdSet using synchronization
   thread 2: store id as a final field into a metric object
   thread 3: read id from them final field and read the array entry
   
   but taking in account _final_ semantic is very special in JMM and _final_ HB does not transitively close with usual HB, I am not sure we can build a proper HB sequence here..
   
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318248340


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();

Review Comment:
   (This is just a suggestion btw)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319833675


##########
src/java/org/apache/cassandra/metrics/MetricClock.java:
##########
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.util.concurrent.TimeUnit;
+
+public class MetricClock extends com.codahale.metrics.Clock
+{
+    @Override
+    public long getTick()
+    {
+        return System.nanoTime(); // checkstyle: permit system clock

Review Comment:
   it's funny, that I initially wrote Global.nanoTime() but then decided to make it closer to the existing behaviour.
   I will switch back to Global.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319913366


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)

Review Comment:
   yes, I am thinking about collecting more stats (how much the overhead in absolute values do we have for monitoring agents) before introducing here an extra complexity...
   I worry mostly not about the metric code complexity itself (while we should be careful here with periodical ticking in meters, where probably will we need to be in sync or use a common logic to do both ticking and snapshotting)
   but about another external code which uses it such as tests, as it may require an analysis of all tests (including python ones) which use metrics to verify/adjust ...
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2370226608


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   > There is one other option I have thought of, that also has an ongoing cost but it should be uniform between systems so we can at least measure it, and it's very simple...
   
   Yes, it is exactly the same thing (in much more details) what I described on a high level as an option 2 :-)
   It requires some time to experiment and measure, so I suggest to do it as a part of the follow-up patch to not delay the current patch applying.
   
   > We could also offset the cost by extending FastThreadLocalThread and making our ThreadLocalMetrics instance an instance variable
   
   yes, it is really a good idea for the follow-up patch! Probably a tracing state also can be a candidate for such optimisation...
   
   So, in total there are the following things for the follow-up patch:
   - to measure overheads for reads and evaluate a batch snapshotting to reduce read costs
   - more reliable recycling logic 
   - I am thinking about trying StampedLock instead of ReadWriteLock
   - Extending FastThreadLocalThread and making ThreadLocalMetrics instance an instance variable
   - explore potential options to reduce memory usage such as using of int to int maps or other more compact representations



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3338041746

   Thank you! 
   Squashed and rebased. 
   org.apache.cassandra.metrics.CassandraHistogram extends ThreadLocalHistogram now


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3327377804

   Hi @belliottsmith, are there any remaining issues you think I should address in the current patch?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319336595


##########
src/java/org/apache/cassandra/metrics/MetricClock.java:
##########
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.util.concurrent.TimeUnit;
+
+public class MetricClock extends com.codahale.metrics.Clock
+{
+    @Override
+    public long getTick()
+    {
+        return System.nanoTime(); // checkstyle: permit system clock

Review Comment:
   This should use `Global.nanoTime()` rather than circumventing the checkstyle restriction (I believe this still breaks the simulator).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319913366


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)

Review Comment:
   yes, I am thinking about collecting more stats (how much the overhead in absolute values do we have for monitoring agents) before introducing here an extra complexity...
   I worry mostly not about the metric code complexity itself (while we should be careful here with periodical ticking in meters, where probably will we need to be in sync or use a common logic to do ticking and snapshotting together, to not amplify discrepancies)
   but about another external code which uses it such as tests, as it may require an analysis of all tests (including python ones) which use metrics to verify/adjust ...
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2317209909


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long summaryLocal;
+        long result;
+        AtomicLong summary = getSummary(metricId);
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            summaryLocal = summary.get();
+            result = 0;
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                if (threadLocalMetrics != null)

Review Comment:
   I think - no, I do not see a possible scenario when it can happen, so removing the check



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316618463


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter

Review Comment:
   Ah, great. Apologies for querying this before I had got that far in review.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2358222199


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   There is one other option I have thought of, that also has an ongoing cost but it should be uniform between systems so we can at least measure it, and it's very simple:
   
   Add a global volatile counter we update whenever we zero and reissue a recycled metric id, and assign the current value of the counter to a final field in the Metric. Inside each instance of ThreadLocalMetrics, track the highest counter we've seen. On read compare the metric counter to the ThreadLocalMetrics counter, and if higher read the global volatile counter and assign the result to our instance counter. This provides a proper happens-before relationship, at the cost of two extra loads (but from adjacent memory) and one extra comparison. We could perhaps reduce the cost further by storing the metric id in a long, and using the top e.g. 40 bits for the counter (and use the same top bits in the volatile counter, so we can compare without transforming), so that we pay only one extra load, one comparison, and one bit mask operation.
   
   The compensative approach should be fine though, and has no ongoing costs, and we have no reason to doubt its correctness so perhaps it should be preferred. But it would be nice to have a way to actually zero the thread local counters reliably.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318279521


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();

Review Comment:
   Oh, you've already done it :) 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2315699138


##########
src/java/org/apache/cassandra/utils/ReflectionUtils.java:
##########
@@ -105,4 +105,36 @@ public static <K, V> void clearMapField(Class<?> clazz, Object instance, String
             throw new RuntimeException(String.format("Could not clear map field %s in class %s", mapName, clazz), ex);
         }
     }
+
+    public static void setFieldToNull(Object object, String fieldName)

Review Comment:
   I think it would be better to accept the declaring class here, so we know we're nulling the correct field while also simplifying the implementation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316617034


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();

Review Comment:
   why use a different object to `freeRateGroupIdSet`?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316660380


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long summaryLocal;
+        long result;
+        AtomicLong summary = getSummary(metricId);
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            summaryLocal = summary.get();
+            result = 0;
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                if (threadLocalMetrics != null)
+                {
+                    long count = 0;
+                    long[] currentCounterValues = threadLocalMetrics.counterValues;
+                    // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                    if (metricId < currentCounterValues.length)
+                        count = currentCounterValues[metricId];
+                    result += count;
+                }
+            }
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        result += summaryLocal;
+        if (resetToZero)
+            summary.addAndGet(-result); // compensative reset without writing to thread local values
+        return result;
+    }
+
+    private static AtomicLong getSummary(int metricId)
+    {
+        AtomicLong result = summaryValues.get(metricId);
+        if (result != null)
+            return result;
+        return summaryValues.computeIfAbsent(metricId, (metricIdToAdd) -> new AtomicLong());
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[(int)(metricId * 1.1)];

Review Comment:
   Math.max(metricId+1, metricId * 1.1)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319877228


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];
+                System.arraycopy(rates, 0, newRates, 0, rates.length);
+                rates = newRates;
+            }
+            rates[rateGroupId +  M1_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId +  M5_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId + M15_RATE_OFFSET] = NON_INITIALIZED;
+        }
+        return rateGroupId;
+    }
+
+    static double getRateValue(int offset)
+    {
+        return rates[offset];
+    }
+
+    private static void setRateValue(int offset, double value)
+    {
+        rates[offset] = value;
+    }
+
+    private final int countMetricId;
+    private final int uncountedMetricId;
+    private final int rateGroupId;
+    private final long startTime;
+    private final MonotonicClock clock;
+    private long lastTick;
+
+    public ThreadLocalMeter()
+    {
+        this(MonotonicClock.Global.approxTime);
+    }
+
+    public ThreadLocalMeter(MonotonicClock clock)
+    {
+        // movingAverages is set to null to reduce metrics memory footprint
+        super(null, Clock.defaultClock());
+        this.clock = clock;
+        this.startTime = this.clock.now();
+        this.lastTick = this.startTime;
+        this.countMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.uncountedMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.rateGroupId = allocateRateGroupOffset();
+        allMeters.add(new WeakReference<>(this));
+        ThreadLocalMetrics.destroyWhenUnreachable(this, new MeterCleaner(countMetricId, uncountedMetricId, rateGroupId));
+        ReflectionUtils.setFieldToNull(this, "count"); // to reduce metrics memory footprint
+    }
+
+    private static class MeterCleaner implements ThreadLocalMetrics.MetricCleaner
+    {
+        private final int countMetricId;
+        private final int uncountedMetricId;
+        private final int rateGroupId;
+
+        private MeterCleaner(int countMetricId, int uncountedMetricId, int rateGroupId)
+        {
+            this.countMetricId = countMetricId;
+            this.uncountedMetricId = uncountedMetricId;
+            this.rateGroupId = rateGroupId;
+        }
+
+        @Override
+        public void clean()
+        {
+            recycleRateGroupId(rateGroupId);
+            ThreadLocalMetrics.recycleMetricId(countMetricId);
+            ThreadLocalMetrics.recycleMetricId(uncountedMetricId);
+        }
+
+        private static void recycleRateGroupId(int rateGroupId)
+        {
+            synchronized (freeRateGroupIdSetGuard)
+            {
+                freeRateGroupIdSet.set(rateGroupId);
+            }
+        }
+    }
+
+    /**
+     * Mark the occurrence of an event.
+     */
+    public void mark()
+    {
+        mark(1);
+    }
+
+    /**
+     * Mark the occurrence of a given number of events.
+     *
+     * @param n the number of events
+     */
+    public void mark(long n)
+    {
+        // we retrieve the context once here to reduce thread local lookup overheads
+        ThreadLocalMetrics context = ThreadLocalMetrics.get();
+        context.addNonStatic(countMetricId, n);
+        context.addNonStatic(uncountedMetricId, n);
+    }
+
+    @Override
+    public long getCount()
+    {
+        return ThreadLocalMetrics.getCount(countMetricId);
+    }
+
+    @Override
+    public double getFifteenMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M15_RATE_OFFSET));
+    }
+
+    @Override
+    public double getFiveMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M5_RATE_OFFSET));
+    }
+
+    @Override
+    public double getOneMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M1_RATE_OFFSET));
+    }
+
+    @Override
+    public double getMeanRate()
+    {
+        long count = getCount();
+        if (count == 0)
+            return 0.0;
+        else
+        {
+            final double elapsed = clock.now() - startTime;
+            return count / elapsed * TimeUnit.SECONDS.toNanos(1);

Review Comment:
   it looks ok for testes, applied



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319877228


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];
+                System.arraycopy(rates, 0, newRates, 0, rates.length);
+                rates = newRates;
+            }
+            rates[rateGroupId +  M1_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId +  M5_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId + M15_RATE_OFFSET] = NON_INITIALIZED;
+        }
+        return rateGroupId;
+    }
+
+    static double getRateValue(int offset)
+    {
+        return rates[offset];
+    }
+
+    private static void setRateValue(int offset, double value)
+    {
+        rates[offset] = value;
+    }
+
+    private final int countMetricId;
+    private final int uncountedMetricId;
+    private final int rateGroupId;
+    private final long startTime;
+    private final MonotonicClock clock;
+    private long lastTick;
+
+    public ThreadLocalMeter()
+    {
+        this(MonotonicClock.Global.approxTime);
+    }
+
+    public ThreadLocalMeter(MonotonicClock clock)
+    {
+        // movingAverages is set to null to reduce metrics memory footprint
+        super(null, Clock.defaultClock());
+        this.clock = clock;
+        this.startTime = this.clock.now();
+        this.lastTick = this.startTime;
+        this.countMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.uncountedMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.rateGroupId = allocateRateGroupOffset();
+        allMeters.add(new WeakReference<>(this));
+        ThreadLocalMetrics.destroyWhenUnreachable(this, new MeterCleaner(countMetricId, uncountedMetricId, rateGroupId));
+        ReflectionUtils.setFieldToNull(this, "count"); // to reduce metrics memory footprint
+    }
+
+    private static class MeterCleaner implements ThreadLocalMetrics.MetricCleaner
+    {
+        private final int countMetricId;
+        private final int uncountedMetricId;
+        private final int rateGroupId;
+
+        private MeterCleaner(int countMetricId, int uncountedMetricId, int rateGroupId)
+        {
+            this.countMetricId = countMetricId;
+            this.uncountedMetricId = uncountedMetricId;
+            this.rateGroupId = rateGroupId;
+        }
+
+        @Override
+        public void clean()
+        {
+            recycleRateGroupId(rateGroupId);
+            ThreadLocalMetrics.recycleMetricId(countMetricId);
+            ThreadLocalMetrics.recycleMetricId(uncountedMetricId);
+        }
+
+        private static void recycleRateGroupId(int rateGroupId)
+        {
+            synchronized (freeRateGroupIdSetGuard)
+            {
+                freeRateGroupIdSet.set(rateGroupId);
+            }
+        }
+    }
+
+    /**
+     * Mark the occurrence of an event.
+     */
+    public void mark()
+    {
+        mark(1);
+    }
+
+    /**
+     * Mark the occurrence of a given number of events.
+     *
+     * @param n the number of events
+     */
+    public void mark(long n)
+    {
+        // we retrieve the context once here to reduce thread local lookup overheads
+        ThreadLocalMetrics context = ThreadLocalMetrics.get();
+        context.addNonStatic(countMetricId, n);
+        context.addNonStatic(uncountedMetricId, n);
+    }
+
+    @Override
+    public long getCount()
+    {
+        return ThreadLocalMetrics.getCount(countMetricId);
+    }
+
+    @Override
+    public double getFifteenMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M15_RATE_OFFSET));
+    }
+
+    @Override
+    public double getFiveMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M5_RATE_OFFSET));
+    }
+
+    @Override
+    public double getOneMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M1_RATE_OFFSET));
+    }
+
+    @Override
+    public double getMeanRate()
+    {
+        long count = getCount();
+        if (count == 0)
+            return 0.0;
+        else
+        {
+            final double elapsed = clock.now() - startTime;
+            return count / elapsed * TimeUnit.SECONDS.toNanos(1);

Review Comment:
   it looks ok for tested, applied



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2356851939


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   Thank you for the ideas. Yes, I thought about the first option too but worry about a cost for ARM processors like AWS Graviton.
   Other options which I have in my mind:
   1) use the compensative write approach to summary which I use currently to provide a reset operation needed for meters, so we don't have to write to `counterValues` during recycling.
   2) Introduce some kind of version/epoch and write it to the higher bits of metric id, increment it when an id is recycling, keep the last seen value for it in `ThreadLocalMetrics` and if a metric id is bigger than the last seen value then so some sync action within a metric updating thread..
   
   The compensative write approach probably looks like the easiest option for me, but it’s also not ideal from a correctness point of view..
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318313495


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];
+                System.arraycopy(rates, 0, newRates, 0, rates.length);
+                rates = newRates;
+            }
+            rates[rateGroupId +  M1_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId +  M5_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId + M15_RATE_OFFSET] = NON_INITIALIZED;
+        }
+        return rateGroupId;
+    }
+
+    static double getRateValue(int offset)
+    {
+        return rates[offset];
+    }
+
+    private static void setRateValue(int offset, double value)
+    {
+        rates[offset] = value;
+    }
+
+    private final int countMetricId;
+    private final int uncountedMetricId;
+    private final int rateGroupId;
+    private final long startTime;
+    private final MonotonicClock clock;
+    private long lastTick;
+
+    public ThreadLocalMeter()
+    {
+        this(MonotonicClock.Global.approxTime);
+    }
+
+    public ThreadLocalMeter(MonotonicClock clock)
+    {
+        // movingAverages is set to null to reduce metrics memory footprint
+        super(null, Clock.defaultClock());
+        this.clock = clock;
+        this.startTime = this.clock.now();
+        this.lastTick = this.startTime;
+        this.countMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.uncountedMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.rateGroupId = allocateRateGroupOffset();
+        allMeters.add(new WeakReference<>(this));
+        ThreadLocalMetrics.destroyWhenUnreachable(this, new MeterCleaner(countMetricId, uncountedMetricId, rateGroupId));
+        ReflectionUtils.setFieldToNull(this, "count"); // to reduce metrics memory footprint
+    }
+
+    private static class MeterCleaner implements ThreadLocalMetrics.MetricCleaner
+    {
+        private final int countMetricId;
+        private final int uncountedMetricId;
+        private final int rateGroupId;
+
+        private MeterCleaner(int countMetricId, int uncountedMetricId, int rateGroupId)
+        {
+            this.countMetricId = countMetricId;
+            this.uncountedMetricId = uncountedMetricId;
+            this.rateGroupId = rateGroupId;
+        }
+
+        @Override
+        public void clean()
+        {
+            recycleRateGroupId(rateGroupId);
+            ThreadLocalMetrics.recycleMetricId(countMetricId);
+            ThreadLocalMetrics.recycleMetricId(uncountedMetricId);
+        }
+
+        private static void recycleRateGroupId(int rateGroupId)
+        {
+            synchronized (freeRateGroupIdSetGuard)
+            {
+                freeRateGroupIdSet.set(rateGroupId);
+            }
+        }
+    }
+
+    /**
+     * Mark the occurrence of an event.
+     */
+    public void mark()
+    {
+        mark(1);
+    }
+
+    /**
+     * Mark the occurrence of a given number of events.
+     *
+     * @param n the number of events
+     */
+    public void mark(long n)
+    {
+        // we retrieve the context once here to reduce thread local lookup overheads
+        ThreadLocalMetrics context = ThreadLocalMetrics.get();
+        context.addNonStatic(countMetricId, n);
+        context.addNonStatic(uncountedMetricId, n);
+    }
+
+    @Override
+    public long getCount()
+    {
+        return ThreadLocalMetrics.getCount(countMetricId);
+    }
+
+    @Override
+    public double getFifteenMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M15_RATE_OFFSET));
+    }
+
+    @Override
+    public double getFiveMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M5_RATE_OFFSET));
+    }
+
+    @Override
+    public double getOneMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M1_RATE_OFFSET));
+    }
+
+    @Override
+    public double getMeanRate()
+    {
+        long count = getCount();
+        if (count == 0)
+            return 0.0;
+        else
+        {
+            final double elapsed = clock.now() - startTime;
+            return count / elapsed * TimeUnit.SECONDS.toNanos(1);
+        }
+    }
+
+    @VisibleForTesting
+    static void tickAll()
+    {
+            List<WeakReference<ThreadLocalMeter>> emptyRefsToRemove = null;
+            for (WeakReference<ThreadLocalMeter> threadLocalMeterRef : allMeters)
+            {
+                ThreadLocalMeter meter = threadLocalMeterRef.get();
+                if (meter != null)
+                    meter.tickIfNessesary();
+                else
+                {
+                    if (emptyRefsToRemove == null)
+                        emptyRefsToRemove = new ArrayList<>();
+                    emptyRefsToRemove.add(threadLocalMeterRef);
+                }
+            }
+            if (emptyRefsToRemove != null)
+                allMeters.removeAll(emptyRefsToRemove);
+
+    }
+
+    private void tickIfNessesary()
+    {
+        long newTick = clock.now();
+        long age = newTick - lastTick;
+        if (age > TICK_INTERVAL_NS)
+        {
+            lastTick = newTick - age % TICK_INTERVAL_NS;
+            long requiredTicks = age / TICK_INTERVAL_NS;
+            tick(requiredTicks);
+        }
+    }
+    private void tick(long requiredTicks)
+    {
+        synchronized (ratesArrayGuard)
+        {
+            if (requiredTicks >= maxTicks)

Review Comment:
   same `if` balancing suggestion as above



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318312432


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];
+                System.arraycopy(rates, 0, newRates, 0, rates.length);
+                rates = newRates;
+            }
+            rates[rateGroupId +  M1_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId +  M5_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId + M15_RATE_OFFSET] = NON_INITIALIZED;
+        }
+        return rateGroupId;
+    }
+
+    static double getRateValue(int offset)
+    {
+        return rates[offset];
+    }
+
+    private static void setRateValue(int offset, double value)
+    {
+        rates[offset] = value;
+    }
+
+    private final int countMetricId;
+    private final int uncountedMetricId;
+    private final int rateGroupId;
+    private final long startTime;
+    private final MonotonicClock clock;
+    private long lastTick;
+
+    public ThreadLocalMeter()
+    {
+        this(MonotonicClock.Global.approxTime);
+    }
+
+    public ThreadLocalMeter(MonotonicClock clock)
+    {
+        // movingAverages is set to null to reduce metrics memory footprint
+        super(null, Clock.defaultClock());
+        this.clock = clock;
+        this.startTime = this.clock.now();
+        this.lastTick = this.startTime;
+        this.countMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.uncountedMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.rateGroupId = allocateRateGroupOffset();
+        allMeters.add(new WeakReference<>(this));
+        ThreadLocalMetrics.destroyWhenUnreachable(this, new MeterCleaner(countMetricId, uncountedMetricId, rateGroupId));
+        ReflectionUtils.setFieldToNull(this, "count"); // to reduce metrics memory footprint
+    }
+
+    private static class MeterCleaner implements ThreadLocalMetrics.MetricCleaner
+    {
+        private final int countMetricId;
+        private final int uncountedMetricId;
+        private final int rateGroupId;
+
+        private MeterCleaner(int countMetricId, int uncountedMetricId, int rateGroupId)
+        {
+            this.countMetricId = countMetricId;
+            this.uncountedMetricId = uncountedMetricId;
+            this.rateGroupId = rateGroupId;
+        }
+
+        @Override
+        public void clean()
+        {
+            recycleRateGroupId(rateGroupId);
+            ThreadLocalMetrics.recycleMetricId(countMetricId);
+            ThreadLocalMetrics.recycleMetricId(uncountedMetricId);
+        }
+
+        private static void recycleRateGroupId(int rateGroupId)
+        {
+            synchronized (freeRateGroupIdSetGuard)
+            {
+                freeRateGroupIdSet.set(rateGroupId);
+            }
+        }
+    }
+
+    /**
+     * Mark the occurrence of an event.
+     */
+    public void mark()
+    {
+        mark(1);
+    }
+
+    /**
+     * Mark the occurrence of a given number of events.
+     *
+     * @param n the number of events
+     */
+    public void mark(long n)
+    {
+        // we retrieve the context once here to reduce thread local lookup overheads
+        ThreadLocalMetrics context = ThreadLocalMetrics.get();
+        context.addNonStatic(countMetricId, n);
+        context.addNonStatic(uncountedMetricId, n);
+    }
+
+    @Override
+    public long getCount()
+    {
+        return ThreadLocalMetrics.getCount(countMetricId);
+    }
+
+    @Override
+    public double getFifteenMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M15_RATE_OFFSET));
+    }
+
+    @Override
+    public double getFiveMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M5_RATE_OFFSET));
+    }
+
+    @Override
+    public double getOneMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M1_RATE_OFFSET));
+    }
+
+    @Override
+    public double getMeanRate()
+    {
+        long count = getCount();
+        if (count == 0)
+            return 0.0;
+        else
+        {
+            final double elapsed = clock.now() - startTime;
+            return count / elapsed * TimeUnit.SECONDS.toNanos(1);
+        }
+    }
+
+    @VisibleForTesting
+    static void tickAll()
+    {
+            List<WeakReference<ThreadLocalMeter>> emptyRefsToRemove = null;
+            for (WeakReference<ThreadLocalMeter> threadLocalMeterRef : allMeters)
+            {
+                ThreadLocalMeter meter = threadLocalMeterRef.get();
+                if (meter != null)

Review Comment:
   suggestion: either pull the first branch up to the same line as the if, or add the braces to match the else branch. I find it easier to read when braces and new lines are balanced, so either:
   
   ```
   if (meter != null) meter.tickIfNessesary();
   else
   {
   ...
   }
   ```
   
   or
   
   ```
   if (meter != null) 
   {
       meter.tickIfNessesary();
   }
   else
   {
   ...
   }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2315706466


##########
src/java/org/apache/cassandra/metrics/Timer.java:
##########
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.time.Duration;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import com.codahale.metrics.Clock;
+import com.codahale.metrics.Metered;
+import com.codahale.metrics.Sampling;
+
+/**
+ * An interface which mimics {@link com.codahale.metrics.Timer} API and allows alternative implementations
+ */
+public interface Timer extends Metered, Sampling
+{
+    /**
+     * A timing context.
+     */
+    class Context implements AutoCloseable
+    {
+        private final Timer timer;
+        private final Clock clock;
+        private final long startTime;
+
+        Context(Timer timer, Clock clock)
+        {
+            this.timer = timer;
+            this.clock = clock;
+            this.startTime = clock.getTick();
+        }
+
+        /**
+         * Updates the timer with the difference between current and start time. Call to this method will
+         * not reset the start time. Multiple calls result in multiple updates.
+         *
+         * @return the elapsed time in nanoseconds
+         */
+        public long stop()
+        {
+            final long elapsed = clock.getTick() - startTime;
+            timer.update(elapsed, TimeUnit.NANOSECONDS);

Review Comment:
   I don't think all of the possible clocks use NANOSECONDS. In fact, the abstract class Clock has its base method returning currentTimeMillis() (don't ask me why). We should perhaps require the units of the clock to be provided to the context, or we should have our own Clock that extends codahale's that tells us the granularity.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2321446874


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[16];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        getSummary(metricId).addAndGet(value);
+                }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)

Review Comment:
   I think it's easier than that - you just run the tests and find out if any fail. I suspect it won't be so hard to resolve :) 
   
   But sure, let's leave it for now. The patch is good and has been in the works for a while.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2315855864


##########
src/java/org/apache/cassandra/metrics/AtomicLongCounter.java:
##########
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.cassandra.utils.ReflectionUtils;
+
+/**
+ * This type of Counter is not very efficient for updates but fast to read.

Review Comment:
   yes, agree, AtomicLong has issues only in highly contended cases.
   Related measurements (a bit old but hopefully still are relevant..): https://psy-lob-saw.blogspot.com/2014/06/jdk8-update-on-scalable-counters.html
   
   I will update the description to be more accurate.
   
   ```
   LongAdder degrades its performance significantly as soon as it experiences any contention, after which we pay higher costs in perpetuity.
   ```
   Do you mean the overhead caused by dealing with the inner cell array which is never shrunk back?
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2317045564


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();

Review Comment:
   it is more like a general habit to not synchronize on objects when an implementation is not under control, mostly inspired by the stories about deadlocks caused by syncrhonizing on some of JDK classes. In this case I think it is safe to syncrhonize on freeRateGroupIdSet or even share the same lock with rates array..



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3338458451

   Ah, sorry, I've misread your first statement about `ShardedHistogram`
   
   So, it looks like we can start with something like this:
   
   ```mermaid
   graph TD;
       OverrideHistogram --> com.codahale.metrics.Histogram;
       ThreadLocalHistogram --> OverrideHistogram;
       ClearableHistogram --> ThreadLocalHistogram;
       ShardedHistogram --> OverrideHistogram;
       OnDemandHistogram --> OverrideHistogram;
   ```
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3336002076

   yes, it makes sense, I've added the delayed recycling logic.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3338105586

   Hmm, I'm not sure that's correct. That would make `ShardedHistogram` extend `ThreadLocalHistogram`. We probably want `ThreadLocalHistogram` to extend `CassandraHistogram`?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3327545794

   Maybe for now we just make the small change to ensure correctness on recycling using either the offset technique, or introducing a delay to recycling (e.g. schedule a task to make the id visible in 1s)? Or else simply disable recycling until we fix it? But, I am not opposed to merging as is - I think the chance of any actual bug appearing there is very low, and we aren't planning an imminent release anyway. But e.g. the delay technique is ~30s of work and basically guarantees correctness in all practical senses. I'll leave it to you to decide.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2355157988


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   Yes. I don't know whether we really want to solve it or not, as it isn't trivial. 
   
   The simplest approach is to introduce some volatile variable we read on each operation. This would ideally be `counterValues` (which is free on x86, but not weaker memory models) but we would need to come up with a scheme to allow us to write a no-op that isn't a no-op (since we cannot actually change `counterValues` under the owning thread). So we might need a second `counterValues0` value we write on the owning thread whenever we resize, and the zeroing thread reads this and writes it to `counterValues` to make the zero visible. This would definitely solve the problem, but it would come with some performance penalty on non-TSO systems.
   
   Another option would be to periodically checkpoint each `ThreadLocalMetrics` object and only recirculate the id after each thread has checkpointed. But this is quite complicated for something that is unlikely to be a bug that could ever manifest in a real system.
   
   Another option would be to zero on release of the id, then simply wait some number of minutes before we mark the id reusable, which more than puts this into "physically impossible even if not technically guaranteed by the JMM" territory.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#issuecomment-3337440850

   Really great patch, +1


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2370226608


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   > There is one other option I have thought of, that also has an ongoing cost but it should be uniform between systems so we can at least measure it, and it's very simple...
   
   Yes, it is exactly the same thing (in much more details) what I described on a high level as an option 2 :-)
   It requires some time to experiment and measure, so I suggest to do it as a part of the follow-up patch to not delay the current patch applying.
   
   > We could also offset the cost by extending FastThreadLocalThread and making our ThreadLocalMetrics instance an instance variable
   
   yes, it is really a good idea for the follow-up patch! Probably a tracing state also can be a candidate for such optimisation...
   
   So, in total there are the following things for the follow-up patch:
   - batch snapshotting to reduce read costs
   - I am thinking about trying StampedLock instead of ReadWriteLock
   - Extending FastThreadLocalThread and making ThreadLocalMetrics instance an instance variable
   - explore potential options to reduce memory usage such as using of int to int maps or other more compact representations



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2370229613


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static volatile AtomicLongArray summaryValues = new AtomicLongArray(INITIAL_COUNTERS_CAPACITY);
+
+    private static final Shutdownable cleaner;
+    private static final Set<PhantomReference<Object>> phantomReferences = Collections.newSetFromMap(new ConcurrentHashMap<>());
+    private static final ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
+
+    static
+    {
+        cleaner = executorFactory().infiniteLoop("ThreadLocalMetrics-Cleaner", ThreadLocalMetrics::cleanupOneReference, UNSAFE);
+    }
+
+    // we assume that counterValues can be only extended
+    private long[] counterValues = new long[INITIAL_COUNTERS_CAPACITY];
+
+    private static void cleanupOneReference() throws InterruptedException
+    {
+        Object obj = referenceQueue.remove(100);
+        if (obj instanceof MetricIdReference)
+        {
+            ((MetricIdReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+        else if (obj instanceof MetricCleanerReference)
+        {
+            ((MetricCleanerReference) obj).release();
+            phantomReferences.remove(obj);
+        }
+    }
+
+    private static class MetricIdReference extends PhantomReference<Object>
+    {
+        private final int metricId;
+
+        public MetricIdReference(Object referent, ReferenceQueue<? super Object> q, int metricId)
+        {
+            super(referent, q);
+            this.metricId = metricId;
+        }
+
+        public void release()
+        {
+            recycleMetricId(metricId);
+        }
+    }
+
+    private static class MetricCleanerReference extends PhantomReference<Object>
+    {
+        private final MetricCleaner metricCleaner;
+
+        public MetricCleanerReference(Object referent, ReferenceQueue<? super Object> q, MetricCleaner metricCleaner)
+        {
+            super(referent, q);
+            this.metricCleaner = metricCleaner;
+        }
+
+        public void release()
+        {
+            metricCleaner.clean();
+        }
+    }
+
+    interface MetricCleaner
+    {
+        void clean();
+    }
+
+    static void destroyWhenUnreachable(Object referent, int metricId)
+    {
+        phantomReferences.add(new MetricIdReference(referent, referenceQueue, metricId));
+    }
+
+    static void destroyWhenUnreachable(Object referent, MetricCleaner metricCleaner)
+    {
+        phantomReferences.add(new MetricCleanerReference(referent, referenceQueue, metricCleaner));
+    }
+
+    @VisibleForTesting
+    public static void shutdownCleaner(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
+    {
+        shutdownAndWait(timeout, unit, of(cleaner));
+    }
+
+    private void release()
+    {
+        // Using this lock while moving we want to avoid races with readers in getCount
+        // such races can cause a transfered value lost or its double-counting by a reader
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            // we may try to release ThreadLocalMetrics 2 times: onRemoval and by PhantomReference
+            // so this if check is needed to avoid a potential double release
+            if (allThreadLocalMetrics.remove(this))
+            {
+                for (int metricId = 0; metricId < counterValues.length; metricId++)
+                {
+                    long value = counterValues[metricId];
+                    if (value != 0)
+                        updateSummary(metricId, value);
+                }
+            }
+        }
+        finally
+        {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * If we already have ThreadLocalMetrics instance looked up for the current thread
+     * we can use this method to avoid thread local lookup costs.
+     * It can be used if you need to update several counters at the same time.
+     * @param metricId metric to add a value
+     * @param n valuen to add, can be negative number as well
+     */
+    public void addNonStatic(int metricId, long n)
+    {
+        getNonStatic(metricId)[metricId] += n;
+    }
+
+    public static void add(int metricId, long n)
+    {
+        get(metricId)[metricId] += n;
+    }
+
+    private static long getCount(int metricId, boolean resetToZero)
+    {
+        long result;
+        Lock readLock = summaryLock.readLock();
+        readLock.lock();
+        try
+        {
+            result = getSummaryValue(metricId);
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long count = 0;
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                // currentCounterValues is extended for a thread when a value for metricId is reported in the thread
+                if (metricId < currentCounterValues.length)
+                    count = currentCounterValues[metricId];
+                result += count;
+            }
+            if (resetToZero)
+                updateSummary(metricId, -result); // compensative reset without writing to thread local values
+        }
+        finally
+        {
+            readLock.unlock();
+        }
+        return result;
+    }
+
+    // must be executed under summaryLock
+    private static long getSummaryValue(int metricId)
+    {
+        return summaryValues.get(metricId);
+    }
+
+    // must be executed under summaryLock
+    private static void updateSummary(int metricId, long value)
+    {
+        summaryValues.getAndAdd(metricId, value);
+    }
+
+    public static long getCount(int metricId)
+    {
+        return getCount(metricId, false);
+    }
+
+    public static long getCountAndReset(int metricId)
+    {
+        return getCount(metricId, true);
+    }
+
+    public static ThreadLocalMetrics get() {
+        return threadLocalMetricsCurrent.get();
+    }
+
+    private static long[] get(int metricId)
+    {
+        ThreadLocalMetrics threadLocalMetrics = ThreadLocalMetrics.get();
+        return threadLocalMetrics.getNonStatic(metricId);
+    }
+
+    private long[] getNonStatic(int metricId)
+    {
+        long[] currentCounterValues = counterValues;
+        if (metricId < currentCounterValues.length)
+            return currentCounterValues;
+
+        long[] newCounterValues = new long[calculateNewCapacity(metricId)];
+        System.arraycopy(currentCounterValues, 0, newCounterValues, 0, currentCounterValues.length);
+        counterValues = newCounterValues;
+        return newCounterValues;
+    }
+
+    private static int calculateNewCapacity(int metricId)
+    {
+        return Math.max(metricId + 1, (int)(metricId * 1.1) );
+    }
+
+    static int allocateMetricId()
+    {
+        int metricId;
+        synchronized (freeMetricIdSetGuard)
+        {
+            metricId = freeMetricIdSet.nextSetBit(0);
+            if (metricId >= 0)
+                freeMetricIdSet.clear(metricId);
+        }
+        if (metricId < 0)
+            metricId = idGenerator.getAndIncrement();
+
+        if (metricId >= summaryValues.length()) // double-checked locking
+        {
+            Lock lock = summaryLock.writeLock();
+            lock.lock();
+            try
+            {
+                if (metricId >= summaryValues.length())
+                {
+                    AtomicLongArray newSummaryValues = new AtomicLongArray(calculateNewCapacity(metricId));
+                    for (int i = 0; i < summaryValues.length(); i++)
+                        newSummaryValues.set(i, summaryValues.get(i));
+                    summaryValues = newSummaryValues;
+                }
+            }
+            finally
+            {
+                lock.unlock();
+            }
+        }
+        return metricId;
+    }
+
+    static void recycleMetricId(int metricId)
+    {
+        // we use lock here to avoid potential issues when a metric is releasing and a thread is detected as dead at the same time
+        // in this case we may clean a summary value and later the thread removal logic may re-add a non-zero summary value
+        Lock lock = summaryLock.writeLock();
+        lock.lock();
+        try
+        {
+            for (ThreadLocalMetrics threadLocalMetrics : allThreadLocalMetrics)
+            {
+                long[] currentCounterValues = threadLocalMetrics.counterValues;
+                if (metricId < currentCounterValues.length)
+                    currentCounterValues[metricId] = 0;

Review Comment:
   > The main one is when the threadLocalMetric resizes itself, which is done in without coordination wrt this operation, and may allow this reset to be lost.
   
   a fix for this issue is committed 



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316133393


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter

Review Comment:
   I'm wondering whether we need to maintain all of the separate rate metrics at the thread level. Since we track the same 1m/5m/15m rates for every Meter, can't we just track the number of ticks in the last x seconds, and then as that rolls over propagate it to shared 1m/5m/15m roll-ups?
   
   I don't have a strong opinion here, I just have always felt it's a bit weird the effort we go to maintaining these rollups for every metric, simply because this is what Codahale defaults to. But, I also don't want to impose lots of extra work if you think the current design makes sense, as it's already a big improvement.
   
   For reference, I am about to merge a version of this for Accord that simply tracks 1m buckets for the past 16m; on read each minute is blended with the prior minute at a ratio based on the time that has elapsed since the last rollover. An approach like this could work, with only the most recent minute being tracked per-thread.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316144735


##########
src/java/org/apache/cassandra/metrics/AtomicLongCounter.java:
##########
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.cassandra.utils.ReflectionUtils;
+
+/**
+ * This type of Counter is not very efficient for updates but fast to read.

Review Comment:
   Right, the cell array has overheads that must be incurred while updating from then on. It's also never able to be as efficient for uncontended operations as it must use CAS rather than e.g. incrementAndGet.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2317052439


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];

Review Comment:
   yes, it is int, I do not remember why do I have this logic :-), probably it is an artefact from an old alternative draft how to extend the array.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2316844176


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();

Review Comment:
   Since we have a summaryLock we could use to resize, should we consider using an AtomicLongArray here?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2318248340


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);
+            return result;
+        }
+
+        // this method is invoked when a thread is going to finish, but it works only for FastThreadLocalThread
+        // so, we use phantom references for other cases
+        @Override
+        protected void onRemoval(ThreadLocalMetrics value)
+        {
+            value.release();
+        }
+    };
+
+    private static final Map<Integer, AtomicLong> summaryValues = new ConcurrentHashMap<>();

Review Comment:
   This is just a suggestion btw, it's not strictly necessary.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319086404


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMeter.java:
##########
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.codahale.metrics.Clock;
+import org.apache.cassandra.concurrent.ScheduledExecutors;
+import org.apache.cassandra.utils.MonotonicClock;
+import org.apache.cassandra.utils.ReflectionUtils;
+
+import static java.lang.Math.exp;
+
+/**
+ * An alternative to Dropwizard Meter which implements the same kind of API.
+ * it has more efficent mark operations and consumes less memory.
+ * Only exponential decaying moving average is supported for 1/5/15-minutes rate values.
+ * Tick logic is moved out from a mark operation and always executed in a background thread.
+ * For better cache locality rate values are extracted to a common non thread-local array
+ *  updated by a background thread in bulk.
+ * Counter logic inside is implemented using @see ThreadLocalMetrics functionality.
+ * NOTE: Dropwizard Meter is a class and there is no an interface for Dropwizard Meter logic,
+ *   so we have to create an alternative hierarchy.
+ */
+public class ThreadLocalMeter extends com.codahale.metrics.Meter implements Meter
+{
+    private static final int INTERVAL_SEC = 5;
+    private static final long TICK_INTERVAL_NS = TimeUnit.SECONDS.toNanos(INTERVAL_SEC);
+    private static final double SECONDS_PER_MINUTE = 60.0;
+    private static final int ONE_MINUTE = 1;
+    private static final int FIVE_MINUTES = 5;
+    private static final int FIFTEEN_MINUTES = 15;
+    private static final double M1_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / ONE_MINUTE);
+    private static final double M5_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIVE_MINUTES);
+    private static final double M15_ALPHA = 1 - exp(-INTERVAL_SEC / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
+
+    // each meter instance stores rate stats using 3 consecutive cells in rates array
+    private static final int M1_RATE_OFFSET = 0;
+    private static final int M5_RATE_OFFSET = 1;
+    private static final int M15_RATE_OFFSET = 2;
+    private static final int RATES_COUNT = 3;
+    private static final double NON_INITIALIZED = Double.MIN_VALUE;
+
+    private static final int BACKGROUND_TICK_INTERVAL_SEC = INTERVAL_SEC;
+    private static final List<WeakReference<ThreadLocalMeter>> allMeters = new CopyOnWriteArrayList<>();
+
+    /**
+     * CASSANDRA-19332
+     * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother
+     * ticking in a loop and instead reset all the EWMAs.
+     */
+    private static final double maxTickZeroTarget = 0.0001;
+    private static final int maxTicks;
+
+    static
+    {
+        int m3Ticks = 1;
+        double emulatedM15Rate = 0.0;
+        emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, Long.MAX_VALUE);
+        do
+        {
+            emulatedM15Rate = tickFifteenMinuteEWMA(emulatedM15Rate, 0);
+            m3Ticks++;
+        }
+        while (getRatePerSecond(emulatedM15Rate) > maxTickZeroTarget);
+        maxTicks = m3Ticks;
+    }
+
+    static
+    {
+        ScheduledExecutors.scheduledTasks.scheduleWithFixedDelay(ThreadLocalMeter::tickAll,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 BACKGROUND_TICK_INTERVAL_SEC,
+                                                                 TimeUnit.SECONDS);
+    }
+
+    private static final Object ratesArrayGuard = new Object();
+
+    // writes should be synchonized using ratesArrayGuard
+    // 1) write during a copy when we need to extend rates array when a new Meter is created
+    // 2) write during a tick
+    private static volatile double[] rates = new double[RATES_COUNT * 16];
+    static final AtomicInteger rateGroupIdGenerator = new AtomicInteger();
+
+    private static final Object freeRateGroupIdSetGuard = new Object();
+
+    // we recycle and reuse rate group IDs
+    // a set bit means the correspondent rate group id is available to use
+    private static final BitSet freeRateGroupIdSet = new BitSet();
+
+    private static int allocateRateGroupOffset()
+    {
+        int rateGroupId;
+        synchronized (freeRateGroupIdSetGuard)
+        {
+            rateGroupId = freeRateGroupIdSet.nextSetBit(0);
+            if (rateGroupId >= 0)
+                freeRateGroupIdSet.clear(rateGroupId);
+        }
+        if (rateGroupId < 0)
+        {
+            rateGroupId = rateGroupIdGenerator.getAndAdd(RATES_COUNT);
+        }
+        synchronized (ratesArrayGuard)
+        {
+            if (rates.length < rateGroupId + RATES_COUNT)
+            {
+                double[] newRates = new double[(int) (rateGroupId + RATES_COUNT)];
+                System.arraycopy(rates, 0, newRates, 0, rates.length);
+                rates = newRates;
+            }
+            rates[rateGroupId +  M1_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId +  M5_RATE_OFFSET] = NON_INITIALIZED;
+            rates[rateGroupId + M15_RATE_OFFSET] = NON_INITIALIZED;
+        }
+        return rateGroupId;
+    }
+
+    static double getRateValue(int offset)
+    {
+        return rates[offset];
+    }
+
+    private static void setRateValue(int offset, double value)
+    {
+        rates[offset] = value;
+    }
+
+    private final int countMetricId;
+    private final int uncountedMetricId;
+    private final int rateGroupId;
+    private final long startTime;
+    private final MonotonicClock clock;
+    private long lastTick;
+
+    public ThreadLocalMeter()
+    {
+        this(MonotonicClock.Global.approxTime);
+    }
+
+    public ThreadLocalMeter(MonotonicClock clock)
+    {
+        // movingAverages is set to null to reduce metrics memory footprint
+        super(null, Clock.defaultClock());
+        this.clock = clock;
+        this.startTime = this.clock.now();
+        this.lastTick = this.startTime;
+        this.countMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.uncountedMetricId = ThreadLocalMetrics.allocateMetricId();
+        this.rateGroupId = allocateRateGroupOffset();
+        allMeters.add(new WeakReference<>(this));
+        ThreadLocalMetrics.destroyWhenUnreachable(this, new MeterCleaner(countMetricId, uncountedMetricId, rateGroupId));
+        ReflectionUtils.setFieldToNull(this, "count"); // to reduce metrics memory footprint
+    }
+
+    private static class MeterCleaner implements ThreadLocalMetrics.MetricCleaner
+    {
+        private final int countMetricId;
+        private final int uncountedMetricId;
+        private final int rateGroupId;
+
+        private MeterCleaner(int countMetricId, int uncountedMetricId, int rateGroupId)
+        {
+            this.countMetricId = countMetricId;
+            this.uncountedMetricId = uncountedMetricId;
+            this.rateGroupId = rateGroupId;
+        }
+
+        @Override
+        public void clean()
+        {
+            recycleRateGroupId(rateGroupId);
+            ThreadLocalMetrics.recycleMetricId(countMetricId);
+            ThreadLocalMetrics.recycleMetricId(uncountedMetricId);
+        }
+
+        private static void recycleRateGroupId(int rateGroupId)
+        {
+            synchronized (freeRateGroupIdSetGuard)
+            {
+                freeRateGroupIdSet.set(rateGroupId);
+            }
+        }
+    }
+
+    /**
+     * Mark the occurrence of an event.
+     */
+    public void mark()
+    {
+        mark(1);
+    }
+
+    /**
+     * Mark the occurrence of a given number of events.
+     *
+     * @param n the number of events
+     */
+    public void mark(long n)
+    {
+        // we retrieve the context once here to reduce thread local lookup overheads
+        ThreadLocalMetrics context = ThreadLocalMetrics.get();
+        context.addNonStatic(countMetricId, n);
+        context.addNonStatic(uncountedMetricId, n);
+    }
+
+    @Override
+    public long getCount()
+    {
+        return ThreadLocalMetrics.getCount(countMetricId);
+    }
+
+    @Override
+    public double getFifteenMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M15_RATE_OFFSET));
+    }
+
+    @Override
+    public double getFiveMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M5_RATE_OFFSET));
+    }
+
+    @Override
+    public double getOneMinuteRate()
+    {
+        return getRatePerSecond(getRateValue(rateGroupId + M1_RATE_OFFSET));
+    }
+
+    @Override
+    public double getMeanRate()
+    {
+        long count = getCount();
+        if (count == 0)
+            return 0.0;
+        else
+        {
+            final double elapsed = clock.now() - startTime;
+            return count / elapsed * TimeUnit.SECONDS.toNanos(1);

Review Comment:
   let me try, I've mimicked the current Meter behaviour mostly because I use Codahale meter as of now as an oracle/model in ThreadLocalMeter unit tests to assert the results of rates calculation.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "netudima (via GitHub)" <gi...@apache.org>.
netudima commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319094037


##########
src/java/org/apache/cassandra/metrics/ThreadLocalMetrics.java:
##########
@@ -0,0 +1,375 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.lang.ref.PhantomReference;
+import java.lang.ref.ReferenceQueue;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLongArray;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import io.netty.util.concurrent.FastThreadLocal;
+import org.apache.cassandra.concurrent.Shutdownable;
+
+import static com.google.common.collect.ImmutableList.of;
+import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory;
+import static org.apache.cassandra.concurrent.InfiniteLoopExecutor.SimulatorSafe.UNSAFE;
+import static org.apache.cassandra.utils.ExecutorUtils.shutdownAndWait;
+
+/**
+ * A thread-local counter implementation designed to use in metrics as an alternative to LongAdder used by Dropwizard metrics.
+ * This implementation has reduced write (increment) CPU usage costs in exchange for a higher read cost.
+ * We keep and increment parts of a counter locally for each thread.
+ * To reduce memory footprint per counter they are grouped together to a long[] array for each thread.
+ * A position of a counter value is the same for every thread for the same counter id.
+ * Piggyback volatile visibility is expected for readers who execute getCount method to see recent writes to thread local arrays.
+ * If a metric is not used anymore the position in the array is reused. Phantom references are used to track aliveness of metric users.
+ * When a thread died the counter values accumulated by it are transfered to a shared summaryValues collection.
+ * Threads death is tracked using 2 approaches: FastThreadLocal.onRemoval callback and phantom references to Thread objects.
+ */
+public class ThreadLocalMetrics
+{
+    private static final int INITIAL_COUNTERS_CAPACITY = 16;
+
+    static final AtomicInteger idGenerator = new AtomicInteger();
+
+    private static final Object freeMetricIdSetGuard = new Object();
+
+    @VisibleForTesting
+    static final BitSet freeMetricIdSet = new BitSet();
+
+    private static final List<ThreadLocalMetrics> allThreadLocalMetrics = new CopyOnWriteArrayList<>();
+
+    /* the lock is used to coordinate the threads which:
+     * 1) transfer values from a dead thread to summaryValues
+     * 2) calculate a getCount value.
+     * Using this lock we want to avoid
+     *  a value lost while moving it in getCount
+     *  as well as a double-counting
+     */
+    private static final ReadWriteLock summaryLock = new ReentrantReadWriteLock();
+
+    private static final FastThreadLocal<ThreadLocalMetrics> threadLocalMetricsCurrent = new FastThreadLocal<>()
+    {
+        @Override
+        protected ThreadLocalMetrics initialValue()
+        {
+            ThreadLocalMetrics result = new ThreadLocalMetrics();
+            allThreadLocalMetrics.add(result);
+            destroyWhenUnreachable(Thread.currentThread(), result::release);

Review Comment:
   do you mean by checking the thread implementation within initialValue() logic to identify if it is FastThreadLocalThread instance?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319336595


##########
src/java/org/apache/cassandra/metrics/MetricClock.java:
##########
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.util.concurrent.TimeUnit;
+
+public class MetricClock extends com.codahale.metrics.Clock
+{
+    @Override
+    public long getTick()
+    {
+        return System.nanoTime(); // checkstyle: permit system clock

Review Comment:
   This should use `Global.nanoTime()` rather than circumventing the checkstyle restriction (this would break the simulator).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


Re: [PR] [CASSANDRA-20250][trunk] Optimize Counter, Meter and Histogram metrics using thread local counters [cassandra]

Posted by "belliottsmith (via GitHub)" <gi...@apache.org>.
belliottsmith commented on code in PR #4324:
URL: https://github.com/apache/cassandra/pull/4324#discussion_r2319336595


##########
src/java/org/apache/cassandra/metrics/MetricClock.java:
##########
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.cassandra.metrics;
+
+import java.util.concurrent.TimeUnit;
+
+public class MetricClock extends com.codahale.metrics.Clock
+{
+    @Override
+    public long getTick()
+    {
+        return System.nanoTime(); // checkstyle: permit system clock

Review Comment:
   This should use `Global.nanoTime()` rather than circumventing the checkstyle restriction (I believe this still breaks the simulator, but either way we should prefer our own mockable clock).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org