diff --git a/.project b/.project index fe8261c37b3..956abcdb4eb 100644 --- a/.project +++ b/.project @@ -14,4 +14,15 @@ org.eclipse.m2e.core.maven2Nature + + + 1783424300172 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + 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 00c55267132..1714a48c795 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 @@ -20,8 +20,8 @@ import java.util.ArrayList; 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; @@ -116,48 +116,59 @@ 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) { + fieldCache.put(item, getField(item, sortField)); } - i++; } - return Collections.unmodifiableList(builder); - } - public Predicate getPredicate() { - return predicate; - } + boolean sortOrderDescending = sortOrder.equalsIgnoreCase(DESCENDING); - public Comparator getComparator() { - return (left, right) -> { + Comparator cachedComparator = (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)) { + Object leftValue = fieldCache.get(left); + Object rightValue = fieldCache.get(right); + if (sortOrderDescending) { + if (rightValue instanceof Comparable r) { return r.compareTo(leftValue); - } else { + } + } else { + if (leftValue instanceof Comparable l) { return l.compareTo(rightValue); } } return 0; } catch (Exception e) { - //LOG.info("Exception sorting destinations", e); return 0; } }; + + collectionList.sort(cachedComparator); + + if (page == -1 || pageSize == -1) { + return 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 collectionList.subList(start, end); + } + + public Predicate getPredicate() { + return predicate; } abstract Object getField(T t, String fieldName); @@ -171,12 +182,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 { - this.sortField = json.getString(SORT_FIELD); - } + if ((json.containsKey(SORT_FIELD)) && json.containsKey(SORT_ORDER)) { + this.sortField = json.getString(SORT_FIELD); + this.sortOrder = json.getString(SORT_ORDER); + } else if (json.containsKey(SORT_COLUMN)) { + this.sortField = json.getString(SORT_COLUMN); this.sortOrder = json.getString(SORT_ORDER); } } @@ -189,7 +199,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/artemis-server/src/test/java/org/apache/activemq/artemis/core/management/impl/view/ViewTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/management/impl/view/ViewTest.java index bd3ec836925..be72f4a3a3b 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/management/impl/view/ViewTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/management/impl/view/ViewTest.java @@ -66,10 +66,6 @@ public String getDefaultOrderColumn() { assertEquals(list.size(), myView.getPagedResult(123, -1).size()); assertEquals(list.size(), myView.getPagedResult(-1, 123).size()); - // page 0 - not really valid but still "works" - assertEquals(0, myView.getPagedResult(0, 123).size()); - - assertEquals(123, myView.getPagedResult(1, 123).size()); // last page 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 00000000000..72af16d52ce --- /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(0, pageSize); + } + + @Benchmark + public List testMiddlePage() { + return view.getPagedResult(middlePage, pageSize); + } + + @Benchmark + public List testAllResults() { + return view.getPagedResult(0, -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