diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ActiveMQAbstractView.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ActiveMQAbstractView.java index 00c552671322..a3a17e67aa6f 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ActiveMQAbstractView.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/management/impl/view/ActiveMQAbstractView.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; @@ -53,8 +54,6 @@ public abstract class ActiveMQAbstractView> private static final String ASCENDING = "asc"; - private static final String DESCENDING = "desc"; - @Deprecated(forRemoval = true) private static final String SORT_COLUMN = "sortColumn"; @@ -116,48 +115,71 @@ public String AsJson(int page, int pageSize) { } public List getPagedResult(int page, int pageSize) { - List builder = new ArrayList<>(); - final int start; - final int end; - if (page == -1 || pageSize == -1) { - start = 0; - end = collection.size(); - } else { - start = (page - 1) * pageSize; - end = Math.min(page * pageSize, collection.size()); + if (collection == null || collection.isEmpty()) { + return List.of(); } - int i = 0; - for (T e : collection.stream().sorted(getComparator()).collect(Collectors.toList())) { - if (i >= start && i < end) { - builder.add(e); + + List collectionList = new ArrayList<>(collection); + + //pre-compute fields once per element + Map fieldCache = new IdentityHashMap<>(collectionList.size()); + for (T item : collectionList) { + if (item != null) { + try { + Object fieldValue = getField(item, sortField); + fieldCache.put(item, fieldValue); + } catch (Exception e) { + //swallow exception and continue + } } - i++; } - return Collections.unmodifiableList(builder); - } - public Predicate getPredicate() { - return predicate; - } + boolean sortOrderAscending = sortOrder.equalsIgnoreCase(ASCENDING); - public Comparator getComparator() { - return (left, right) -> { - try { - Object leftValue = getField(left, sortField); - Object rightValue = getField(right, sortField); - if (leftValue instanceof Comparable l && rightValue instanceof Comparable r) { - if (sortOrder.equalsIgnoreCase(DESCENDING)) { - return r.compareTo(leftValue); - } else { - return l.compareTo(rightValue); - } - } - return 0; - } catch (Exception e) { - //LOG.info("Exception sorting destinations", e); + Comparator cachedComparator = (left, right) -> { + Object leftValue = fieldCache.get(left); + Object rightValue = fieldCache.get(right); + + if (leftValue == rightValue) { return 0; } + // push nulls to bottom of the list + if (leftValue == null) { + return 1; + } + if (rightValue == null) { + return -1; + } + + if (leftValue instanceof Comparable l && rightValue instanceof Comparable r) { + if (sortOrderAscending) { + return l.compareTo(rightValue); + } else { + return r.compareTo(leftValue); + } + } + + return 0; }; + + collectionList.sort(cachedComparator); + + if (page == -1 || pageSize == -1) { + return Collections.unmodifiableList(collectionList); + } + + int start = (page - 1) * pageSize; + int size = collectionList.size(); + if (start >= size || start < 0) { + return List.of(); + } + int end = Math.min(page * pageSize, size); + + return Collections.unmodifiableList(collectionList.subList(start, end)); + } + + public Predicate getPredicate() { + return predicate; } abstract Object getField(T t, String fieldName); @@ -171,11 +193,11 @@ public void setOptions(String options) { } if (predicate != null) { predicate.addFilterParts(createFilterPredicates(json)); - if ((json.containsKey(SORT_COLUMN) || json.containsKey(SORT_FIELD)) && json.containsKey(SORT_ORDER)) { - if (json.containsKey(SORT_COLUMN)) { - this.sortField = json.getString(SORT_COLUMN); - } else { + if (json.containsKey(SORT_ORDER)) { + if (json.containsKey(SORT_FIELD)) { this.sortField = json.getString(SORT_FIELD); + } else if (json.containsKey(SORT_COLUMN)) { + this.sortField = json.getString(SORT_COLUMN); } this.sortOrder = json.getString(SORT_ORDER); } @@ -189,7 +211,8 @@ private List createFilterPredicates(JsonObject json) { predicates.add(predicate.createFilterPart(json.getString(FILTER_FIELD), json.getString(FILTER_OPERATION), json.getString(FILTER_VALUE))); } else { for (JsonValue jsonValue : jsonArray) { - predicates.add(predicate.createFilterPart(((JsonObject)jsonValue).getString(FILTER_FIELD), ((JsonObject)jsonValue).getString(FILTER_OPERATION), ((JsonObject)jsonValue).getString(FILTER_VALUE))); + JsonObject jsonObject = (JsonObject) jsonValue; + predicates.add(predicate.createFilterPart(jsonObject.getString(FILTER_FIELD), jsonObject.getString(FILTER_OPERATION), jsonObject.getString(FILTER_VALUE))); } } return predicates; diff --git a/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/AbstractViewPerfTest.java b/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/AbstractViewPerfTest.java new file mode 100644 index 000000000000..62ee4707ce77 --- /dev/null +++ b/tests/performance-jmh/src/main/java/org/apache/activemq/artemis/tests/performance/jmh/AbstractViewPerfTest.java @@ -0,0 +1,239 @@ +/* + * 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.activemq.artemis.tests.performance.jmh; + +import org.apache.activemq.artemis.api.core.management.AddressControl; +import org.apache.activemq.artemis.core.management.impl.view.AddressField; +import org.apache.activemq.artemis.core.management.impl.view.AddressView; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.Mode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@Fork(value = 1) +@Warmup(iterations = 2, time = 1) +@Measurement(iterations = 5, time = 1) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public class AbstractViewPerfTest { + + @Param({"1000", "10000"}) + private int addressCount; + + @Param({"50"}) + private int pageSize; + + private AddressView view; + private int middlePage; + + @Setup(Level.Trial) + public void init() { + view = new AddressView(null); + + // 1. Configure the view options using the internal JSON parsing mechanism + // This switches the target field to MESSAGE_COUNT and sets the sorting loop active + String optionsJson = "{" + + "\"field\":\"\"," + + "\"operation\":\"\"," + + "\"value\":\"\"," + + "\"sortField\":\"" + AddressField.MESSAGE_COUNT.getName() + "\"," + + "\"sortOrder\":\"desc\"" + + "}"; + view.setOptions(optionsJson); + + // 2. Generate data rows + List addresses = new ArrayList<>(addressCount); + for (int i = 0; i < addressCount; i++) { + addresses.add(stubAddress(i)); + } + + // 3. Shuffle dataset to break TimSort's best-case O(N) shortcut paths + Collections.shuffle(addresses); + + view.setCollection(addresses); + middlePage = (addressCount / 2) / pageSize + 1; + } + + @Benchmark + public List testFirstPage() { + return view.getPagedResult(1, pageSize); + } + + @Benchmark + public List testMiddlePage() { + return view.getPagedResult(middlePage, pageSize); + } + + @Benchmark + public List testAllResults() { + return view.getPagedResult(-1, -1); + } + + private static AddressControl stubAddress(long id) { + final long simulatedMessageCount = (id * 31) % 10000; + final String addressName = "address-" + id; + + return new AddressControl() { + @Override public long getId() { + return id; + } + @Override public String getAddress() { + return addressName; + } + + @Override + public long getMessageCount() { + // Mimic production cost: CPU loop simulating reading multiple live + // internal queue components and consolidating dynamic tracking fields. + long infrastructureTraversalCost = 0; + for (int i = 0; i < 4; i++) { + infrastructureTraversalCost += Thread.currentThread().hashCode() + simulatedMessageCount; + } + return simulatedMessageCount + (infrastructureTraversalCost & 0); + } + + @Override public String[] getRoutingTypes() { + return new String[0]; + } + @Override public String getRoutingTypesAsJSON() { + return "[]"; + } + @Override public Object[] getRoles() { + return new Object[0]; + } + @Override public String getRolesAsJSON() { + return "[]"; + } + @Override public long getAddressSize() { + return 0L; + } + @Override public int getMaxPageReadBytes() { + return 0; + } + @Override public int getMaxPageReadMessages() { + return 0; + } + @Override public int getPrefetchPageBytes() { + return 0; + } + @Override public int getPrefetchPageMessages() { + return 0; + } + @Override public void schedulePageCleanup() { + } + @Override public long getNumberOfMessages() { + return 0L; + } + @Override public String[] getRemoteQueueNames() { + return new String[0]; + } + @Override public String[] getQueueNames() { + return new String[0]; + } + @Override public String[] getAllQueueNames() { + return new String[0]; + } + @Override public long getNumberOfPages() { + return 0L; + } + @Override public boolean isPaging() { + return false; + } + @Override public int getAddressLimitPercent() { + return 0; + } + @Override public boolean block() { + return false; + } + @Override public void unblock() { + } + @Override public boolean isBlockedViaManagement() { + return false; + } + @Override public long getNumberOfBytesPerPage() { + return 0L; + } + @Override public String[] getBindingNames() { + return new String[0]; + } + @Override public long getQueueCount() { + return 0L; + } + @Override public long getRoutedMessageCount() { + return 0L; + } + @Override public long getUnRoutedMessageCount() { + return 0L; + } + @Override public void pause() { + } + @Override public void pause(boolean persist) { + } + @Override public void resume() { + } + @Override public boolean isPaused() { + return false; + } + @Override public boolean isRetroactiveResource() { + return false; + } + @Override public long getCurrentDuplicateIdCacheSize() { + return 0L; + } + @Override public boolean clearDuplicateIdCache() { + return false; + } + @Override public boolean isAutoCreated() { + return false; + } + @Override public boolean isInternal() { + return false; + } + @Override public boolean isTemporary() { + return false; + } + @Override public long purge() { + return 0L; + } + @Override public void replay(String target, String filter) { + } + @Override public void replay(String startScan, String endScan, String target, String filter) { + } + @Override public String sendMessage(Map headers, int type, String body, boolean durable, String user, String password) { + return null; + } + @Override public String sendMessage(Map headers, int type, String body, boolean durable, String user, String password, boolean createMessageId) { + return null; + } + }; + } +} \ No newline at end of file