From c9a0d6e7c4dd0c1815be0a3510e7b132a9e64cf0 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 4 Sep 2025 14:44:21 +0200 Subject: [PATCH 1/9] HHH-19240 Test memory consumption of HQL parser (cherry picked from commit 97d87c494c20e8a63ccacf74a2b6dad2382d3644) --- .../test/hql/HqlParserMemoryUsageTest.java | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java new file mode 100644 index 000000000000..c5bd4f3118a9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java @@ -0,0 +1,216 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.query.hql.HqlTranslator; +import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( + annotatedClasses = { + HqlParserMemoryUsageTest.Address.class, + HqlParserMemoryUsageTest.AppUser.class, + HqlParserMemoryUsageTest.Category.class, + HqlParserMemoryUsageTest.Discount.class, + HqlParserMemoryUsageTest.Order.class, + HqlParserMemoryUsageTest.OrderItem.class, + HqlParserMemoryUsageTest.Product.class + } +) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.QUERY_PLAN_CACHE_ENABLED, value = "false")) +@Jira("https://hibernate.atlassian.net/browse/HHH-19240") +public class HqlParserMemoryUsageTest { + + private static final String HQL = """ + SELECT DISTINCT u.id + FROM AppUser u + LEFT JOIN u.addresses a + LEFT JOIN u.orders o + LEFT JOIN o.orderItems oi + LEFT JOIN oi.product p + LEFT JOIN p.discounts d + WHERE u.id = :userId + AND ( + CASE + WHEN u.name = 'SPECIAL_USER' THEN TRUE + ELSE ( + CASE + WHEN a.city = 'New York' THEN TRUE + ELSE ( + p.category.name = 'Electronics' + OR d.code LIKE '%DISC%' + OR u.id IN ( + SELECT u2.id + FROM AppUser u2 + JOIN u2.orders o2 + JOIN o2.orderItems oi2 + JOIN oi2.product p2 + WHERE p2.price > ( + SELECT AVG(p3.price) FROM Product p3 + ) + ) + ) + END + ) + END + ) + """; + + + @Test + public void testParserMemoryUsage(SessionFactoryScope scope) { + final HqlTranslator hqlTranslator = scope.getSessionFactory().getQueryEngine().getHqlTranslator(); + final Runtime runtime = Runtime.getRuntime(); + + // Ensure classes and basic stuff is initialized in case this is the first test run + hqlTranslator.translate( "from AppUser", AppUser.class ); + runtime.gc(); + runtime.gc(); + + // Track memory usage before execution + long totalMemoryBefore = runtime.totalMemory(); + long usedMemoryBefore = totalMemoryBefore - runtime.freeMemory(); + + System.out.println("Memory Usage Before Create Query:"); + System.out.println("----------------------------"); + System.out.println("Total Memory: " + (totalMemoryBefore / 1024) + " KB"); + System.out.println("Used Memory : " + (usedMemoryBefore / 1024) + " KB"); + System.out.println(); + + // Create query + SqmStatement statement = hqlTranslator.translate( HQL, Long.class ); + + // Track memory usage after execution + long totalMemoryAfter = runtime.totalMemory(); + long usedMemoryAfter = totalMemoryAfter - runtime.freeMemory(); + + System.out.println("Memory Usage After Create Query:"); + System.out.println("----------------------------"); + System.out.println("Total Memory: " + (totalMemoryAfter / 1024) + " KB"); + System.out.println("Used Memory : " + (usedMemoryAfter / 1024) + " KB"); + System.out.println(); + + System.out.println("Memory increase After Parsing:"); + System.out.println("----------------------------"); + System.out.println("Total Memory increase: " + ((totalMemoryAfter - totalMemoryBefore) / 1024) + " KB"); + System.out.println("Used Memory increase : " + ((usedMemoryAfter - usedMemoryBefore) / 1024) + " KB"); + System.out.println(); + + runtime.gc(); + runtime.gc(); + + // Track memory usage after execution + long totalMemoryAfterGc = runtime.totalMemory(); + long usedMemoryAfterGc = totalMemoryAfterGc - runtime.freeMemory(); + + System.out.println("Memory Usage After Create Query and GC:"); + System.out.println("----------------------------"); + System.out.println("Total Memory: " + (totalMemoryAfterGc / 1024) + " KB"); + System.out.println("Used Memory : " + (usedMemoryAfterGc / 1024) + " KB"); + System.out.println(); + + System.out.println("Memory overhead of Parsing:"); + System.out.println("----------------------------"); + System.out.println("Total Memory increase: " + ((totalMemoryAfter - totalMemoryAfterGc) / 1024) + " KB"); + System.out.println("Used Memory increase : " + ((usedMemoryAfter - usedMemoryAfterGc) / 1024) + " KB"); + System.out.println(); + + // During testing, before the fix for HHH-19240, the allocation was around 500+ MB, + // and after the fix it dropped to 170 - 250 MB + final long memoryConsumption = usedMemoryAfter - usedMemoryAfterGc; + assertTrue( usedMemoryAfter - usedMemoryAfterGc < 256_000_000, "Parsing of queries consumes too much memory (" + ( memoryConsumption / 1024 ) + " KB), when at most 256 MB are expected" ); + } + + @Entity(name = "Address") + @Table(name = "addresses") + public static class Address { + @Id + private Long id; + private String city; + @ManyToOne(fetch = FetchType.LAZY) + private AppUser user; + } + @Entity(name = "AppUser") + @Table(name = "app_users") + public static class AppUser { + @Id + private Long id; + private String name; + @OneToMany(mappedBy = "user") + private Set
addresses; + @OneToMany(mappedBy = "user") + private Set orders; + } + + @Entity(name = "Category") + @Table(name = "categories") + public static class Category { + @Id + private Long id; + private String name; + } + + @Entity(name = "Discount") + @Table(name = "discounts") + public static class Discount { + @Id + private Long id; + private String code; + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + } + + @Entity(name = "Order") + @Table(name = "orders") + public static class Order { + @Id + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private AppUser user; + @OneToMany(mappedBy = "order") + private Set orderItems; + } + @Entity(name = "OrderItem") + @Table(name = "order_items") + public static class OrderItem { + @Id + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private Order order; + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + } + + @Entity(name = "Product") + @Table(name = "products") + public static class Product { + @Id + private Long id; + private String name; + private Double price; + @ManyToOne(fetch = FetchType.LAZY) + private Category category; + @OneToMany(mappedBy = "product") + private Set discounts; + } +} From 71ce6f587b9059c5d7bca78fd07445a3f59a6b22 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 4 Sep 2025 14:44:47 +0200 Subject: [PATCH 2/9] HHH-19240 Reduce memory consumption by left factoring some HQL parse rules (cherry picked from commit 0692e0c65364029e4f579a9fe955a68d40b32b32) --- .../org/hibernate/grammars/hql/HqlParser.g4 | 16 ++--- .../hql/internal/SemanticQueryBuilder.java | 62 +++++++++---------- 2 files changed, 37 insertions(+), 41 deletions(-) diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 5db8339e9a72..2708d1a09ce9 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -427,8 +427,6 @@ pathContinuation * * VALUE( path ) * * KEY( path ) * * path[ selector ] - * * ARRAY_GET( embeddableArrayPath, index ).path - * * COALESCE( array1, array2 )[ selector ].path */ syntacticDomainPath : treatedNavigablePath @@ -436,10 +434,6 @@ syntacticDomainPath | mapKeyNavigablePath | simplePath indexedPathAccessFragment | simplePath slicedPathAccessFragment - | toOneFkReference - | function pathContinuation - | function indexedPathAccessFragment pathContinuation? - | function slicedPathAccessFragment ; /** @@ -748,7 +742,14 @@ primaryExpression | entityVersionReference # EntityVersionExpression | entityNaturalIdReference # EntityNaturalIdExpression | syntacticDomainPath pathContinuation? # SyntacticPathExpression - | function # FunctionExpression + // ARRAY_GET( embeddableArrayPath, index ).path + // COALESCE( array1, array2 )[ selector ].path + // COALESCE( array1, array2 )[ start : end ] + | function ( + pathContinuation + | slicedPathAccessFragment + | indexedPathAccessFragment pathContinuation? + )? # FunctionExpression | generalPathFragment # GeneralPathExpression ; @@ -1108,6 +1109,7 @@ function | columnFunction | jsonFunction | xmlFunction + | toOneFkReference | genericFunction ; diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 6ef1f0f40493..72159bc3c198 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -1860,8 +1860,34 @@ public Object visitGeneralPathExpression(HqlParser.GeneralPathExpressionContext } @Override - public SqmExpression visitFunctionExpression(HqlParser.FunctionExpressionContext ctx) { - return (SqmExpression) ctx.function().accept( this ); + public Object visitFunctionExpression(HqlParser.FunctionExpressionContext ctx) { + final var slicedFragmentsCtx = ctx.slicedPathAccessFragment(); + if ( slicedFragmentsCtx != null ) { + final List slicedFragments = slicedFragmentsCtx.expression(); + return getFunctionDescriptor( "array_slice" ).generateSqmExpression( + List.of( + (SqmTypedNode) visitFunction( ctx.function() ), + (SqmTypedNode) slicedFragments.get( 0 ).accept( this ), + (SqmTypedNode) slicedFragments.get( 1 ).accept( this ) + ), + null, + queryEngine() + ); + } + else { + final var function = (SqmExpression) visitFunction( ctx.function() ); + final var indexedPathAccessFragment = ctx.indexedPathAccessFragment(); + final var pathContinuation = ctx.pathContinuation(); + if ( indexedPathAccessFragment == null && pathContinuation == null ) { + return function; + } + else { + return visitPathContinuation( + visitIndexedPathAccessFragment( (SemanticPathPart) function, indexedPathAccessFragment ), + pathContinuation + ); + } + } } @Override @@ -3548,11 +3574,6 @@ else if ( attributes.size() >1 ) { throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + "' of 'naturalid()' does not resolve to an entity type" ); } -// -// @Override -// public Object visitToOneFkExpression(HqlParser.ToOneFkExpressionContext ctx) { -// return visitToOneFkReference( (HqlParser.ToOneFkReferenceContext) ctx.getChild( 0 ) ); -// } @Override public SqmFkExpression visitToOneFkReference(HqlParser.ToOneFkReferenceContext ctx) { @@ -5719,33 +5740,6 @@ else if ( ctx.collectionValueNavigablePath() != null ) { else if ( ctx.mapKeyNavigablePath() != null ) { return visitMapKeyNavigablePath( ctx.mapKeyNavigablePath() ); } - else if ( ctx.toOneFkReference() != null ) { - return visitToOneFkReference( ctx.toOneFkReference() ); - } - else if ( ctx.function() != null ) { - final var slicedFragmentsCtx = ctx.slicedPathAccessFragment(); - if ( slicedFragmentsCtx != null ) { - final List slicedFragments = slicedFragmentsCtx.expression(); - return getFunctionDescriptor( "array_slice" ).generateSqmExpression( - List.of( - (SqmTypedNode) visitFunction( ctx.function() ), - (SqmTypedNode) slicedFragments.get( 0 ).accept( this ), - (SqmTypedNode) slicedFragments.get( 1 ).accept( this ) - ), - null, - queryEngine() - ); - } - else { - return visitPathContinuation( - visitIndexedPathAccessFragment( - (SemanticPathPart) visitFunction( ctx.function() ), - ctx.indexedPathAccessFragment() - ), - ctx.pathContinuation() - ); - } - } else if ( ctx.simplePath() != null && ctx.indexedPathAccessFragment() != null ) { return visitIndexedPathAccessFragment( visitSimplePath( ctx.simplePath() ), ctx.indexedPathAccessFragment() ); } From b18951e31deadeb5ccefd5b41629fe410a58a044 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 8 Sep 2025 14:24:53 +0200 Subject: [PATCH 3/9] HHH-19240 Simplify queryExpression grammar rule (cherry picked from commit 0e158e0bfbbc4d1c33a1b2a81dcf3b386f71efb8) --- .../org/hibernate/grammars/hql/HqlParser.g4 | 3 +- .../hql/internal/SemanticQueryBuilder.java | 32 +++++++------------ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 2708d1a09ce9..4d3f9e9f18b5 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -151,8 +151,7 @@ cycleClause * A toplevel query of subquery, which may be a union or intersection of subqueries */ queryExpression - : withClause? orderedQuery # SimpleQueryGroup - | withClause? orderedQuery (setOperator orderedQuery)+ # SetQueryGroup + : withClause? orderedQuery (setOperator orderedQuery)* ; /** diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 72159bc3c198..7e2925898a01 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -775,12 +775,10 @@ public Object visitCte(HqlParser.CteContext ctx) { final JpaCteCriteria oldCte = currentPotentialRecursiveCte; try { currentPotentialRecursiveCte = null; - if ( queryExpressionContext instanceof HqlParser.SetQueryGroupContext setContext ) { - // A recursive query is only possible if the child count is lower than 5 e.g. `withClause? q1 op q2` - if ( setContext.getChildCount() < 5 ) { - if ( handleRecursive( ctx, setContext, cteContainer, name, cte, materialization ) ) { - return null; - } + // A recursive query is only possible if there are 2 ordered queries e.g. `q1 op q2` + if ( queryExpressionContext.orderedQuery().size() == 2 ) { + if ( handleRecursive( ctx, queryExpressionContext, cteContainer, name, cte, materialization ) ) { + return null; } } queryExpressionContext.accept( this ); @@ -798,7 +796,7 @@ public Object visitCte(HqlParser.CteContext ctx) { private boolean handleRecursive( HqlParser.CteContext cteContext, - HqlParser.SetQueryGroupContext setContext, + HqlParser.QueryExpressionContext setContext, SqmCteContainer cteContainer, String name, SqmSelectQuery cte, @@ -967,15 +965,6 @@ private static CteSearchClauseKind getCteSearchClauseKind(HqlParser.SearchClause return ctx.BREADTH() != null ? CteSearchClauseKind.BREADTH_FIRST : CteSearchClauseKind.DEPTH_FIRST; } - @Override - public SqmQueryPart visitSimpleQueryGroup(HqlParser.SimpleQueryGroupContext ctx) { - final var withClauseContext = ctx.withClause(); - if ( withClauseContext != null ) { - withClauseContext.accept( this ); - } - return (SqmQueryPart) ctx.orderedQuery().accept( this ); - } - @Override public SqmQueryPart visitQueryOrderExpression(HqlParser.QueryOrderExpressionContext ctx) { final SqmQuerySpec sqmQuerySpec = currentQuerySpec(); @@ -1012,25 +1001,28 @@ public SqmQueryPart visitNestedQueryExpression(HqlParser.NestedQueryExpressio } @Override - public SqmQueryGroup visitSetQueryGroup(HqlParser.SetQueryGroupContext ctx) { + public SqmQueryPart visitQueryExpression(HqlParser.QueryExpressionContext ctx) { var withClauseContext = ctx.withClause(); if ( withClauseContext != null ) { withClauseContext.accept( this ); } + final var orderedQueryContexts = ctx.orderedQuery(); final SqmQueryPart firstQueryPart = - (SqmQueryPart) ctx.orderedQuery(0).accept( this ); + (SqmQueryPart) orderedQueryContexts.get( 0 ).accept( this ); + if ( orderedQueryContexts.size() == 1 ) { + return firstQueryPart; + } SqmQueryGroup queryGroup = firstQueryPart instanceof SqmQueryGroup sqmQueryGroup ? sqmQueryGroup : new SqmQueryGroup<>( firstQueryPart ); setCurrentQueryPart( queryGroup ); - final var orderedQueryContexts = ctx.orderedQuery(); final var setOperatorContexts = ctx.setOperator(); final SqmCreationProcessingState firstProcessingState = processingStateStack.pop(); for ( int i = 0; i < setOperatorContexts.size(); i++ ) { queryGroup = getSqmQueryGroup( visitSetOperator( setOperatorContexts.get(i) ), - orderedQueryContexts.get( i+1 ), + orderedQueryContexts.get( i + 1 ), queryGroup, setOperatorContexts.size(), firstProcessingState, From 148a0b181d3a71a0c2fb56232cdc2f5d6d18883e Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 9 Sep 2025 10:20:29 +0200 Subject: [PATCH 4/9] HHH-19758 Don't reset lexer on SLL parse error (cherry picked from commit 39ac25594ad91e90c1003d439f8fd0099345da9a) --- .../mapping/ordering/OrderByFragmentTranslator.java | 7 ++++++- .../query/hql/internal/StandardHqlTranslator.java | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ordering/OrderByFragmentTranslator.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ordering/OrderByFragmentTranslator.java index b1344d38e757..43c784348e90 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ordering/OrderByFragmentTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ordering/OrderByFragmentTranslator.java @@ -73,8 +73,13 @@ private static OrderingParser.OrderByFragmentContext buildParseTree(String fragm return parser.orderByFragment(); } catch (ParseCancellationException e) { + // When resetting the parser, its CommonTokenStream will seek(0) i.e. restart emitting buffered tokens. + // This is enough when reusing the lexer and parser, and it would be wrong to also reset the lexer. + // Resetting the lexer causes it to hand out tokens again from the start, which will then append to the + // CommonTokenStream and cause a wrong parse + // lexer.reset(); + // reset the input token stream and parser state - lexer.reset(); parser.reset(); // fall back to LL(k)-based parsing diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/StandardHqlTranslator.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/StandardHqlTranslator.java index a40c1589fdc1..3b6eef1846c7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/StandardHqlTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/StandardHqlTranslator.java @@ -104,9 +104,14 @@ private HqlParser.StatementContext parseHql(String hql) { try { return hqlParser.statement(); } - catch ( ParseCancellationException e) { + catch (ParseCancellationException e) { + // When resetting the parser, its CommonTokenStream will seek(0) i.e. restart emitting buffered tokens. + // This is enough when reusing the lexer and parser, and it would be wrong to also reset the lexer. + // Resetting the lexer causes it to hand out tokens again from the start, which will then append to the + // CommonTokenStream and cause a wrong parse + // hqlLexer.reset(); + // reset the input token stream and parser state - hqlLexer.reset(); hqlParser.reset(); // fall back to LL(k)-based parsing From bc30cdc7a523776f131d1a4876340ea98877267d Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 8 Sep 2025 14:32:49 +0200 Subject: [PATCH 5/9] HHH-19240 Refactor IS predicate to single UnaryIsPredicate rule (cherry picked from commit adcb5dbed53b819535fb39c0d1063214b0f2aa6b) --- .../org/hibernate/grammars/hql/HqlParser.g4 | 5 +- .../hql/internal/SemanticQueryBuilder.java | 60 ++++++------------- 2 files changed, 19 insertions(+), 46 deletions(-) diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 4d3f9e9f18b5..32aaf9bf8b89 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -654,10 +654,7 @@ whereClause predicate //highest to lowest precedence : LEFT_PAREN predicate RIGHT_PAREN # GroupedPredicate - | expression IS NOT? NULL # IsNullPredicate - | expression IS NOT? EMPTY # IsEmptyPredicate - | expression IS NOT? TRUE # IsTruePredicate - | expression IS NOT? FALSE # IsFalsePredicate + | expression IS NOT? (NULL|EMPTY|TRUE|FALSE) # UnaryIsPredicate | expression IS NOT? DISTINCT FROM expression # IsDistinctFromPredicate | expression NOT? MEMBER OF? path # MemberOfPredicate | expression NOT? IN inList # InPredicate diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 7e2925898a01..c9723c4865a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -2439,49 +2439,25 @@ public SqmBetweenPredicate visitBetweenPredicate(HqlParser.BetweenPredicateConte ); } - - @Override - public SqmNullnessPredicate visitIsNullPredicate(HqlParser.IsNullPredicateContext ctx) { - return new SqmNullnessPredicate( - (SqmExpression) ctx.expression().accept( this ), - ctx.NOT() != null, - nodeBuilder() - ); - } - - @Override - public SqmEmptinessPredicate visitIsEmptyPredicate(HqlParser.IsEmptyPredicateContext ctx) { - SqmExpression expression = (SqmExpression) ctx.expression().accept(this); - if ( expression instanceof SqmPluralValuedSimplePath pluralValuedSimplePath ) { - return new SqmEmptinessPredicate( - pluralValuedSimplePath, - ctx.NOT() != null, - nodeBuilder() - ); - } - else { - throw new SemanticException( "Operand of 'is empty' operator must be a plural path", query ); - } - } - @Override - public Object visitIsTruePredicate(HqlParser.IsTruePredicateContext ctx) { - return new SqmTruthnessPredicate( - (SqmExpression) ctx.expression().accept( this ), - true, - ctx.NOT() != null, - nodeBuilder() - ); - } - - @Override - public Object visitIsFalsePredicate(HqlParser.IsFalsePredicateContext ctx) { - return new SqmTruthnessPredicate( - (SqmExpression) ctx.expression().accept( this ), - false, - ctx.NOT() != null, - nodeBuilder() - ); + public SqmPredicate visitUnaryIsPredicate(HqlParser.UnaryIsPredicateContext ctx) { + final var expression = (SqmExpression) ctx.expression().accept( this ); + final var negated = ctx.NOT() != null; + final var nodeBuilder = nodeBuilder(); + return switch ( ((TerminalNode) ctx.getChild( ctx.getChildCount() - 1 )).getSymbol().getType() ) { + case HqlParser.NULL -> new SqmNullnessPredicate( expression, negated, nodeBuilder ); + case HqlParser.EMPTY -> { + if ( expression instanceof SqmPluralValuedSimplePath pluralValuedSimplePath ) { + yield new SqmEmptinessPredicate( pluralValuedSimplePath, negated, nodeBuilder ); + } + else { + throw new SemanticException( "Operand of 'is empty' operator must be a plural path", query ); + } + } + case HqlParser.TRUE -> new SqmTruthnessPredicate( expression, true, negated, nodeBuilder ); + case HqlParser.FALSE -> new SqmTruthnessPredicate( expression, false, negated, nodeBuilder ); + default -> throw new AssertionError( "Unknown unary is predicate: " + ctx.getChild( ctx.getChildCount() - 1 ) ); + }; } @Override From d8fcd1bfd639690dd487611db9f8ef0bd8194503 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 8 Sep 2025 14:36:45 +0200 Subject: [PATCH 6/9] HHH-19240 Left-factor CONTAINS/INCLUDES/INTERSECTS to single BinaryExpressionPredicate rule (cherry picked from commit 1eecda87e95ecd580029df7484981d0eea927f74) --- .../org/hibernate/grammars/hql/HqlParser.g4 | 4 +- .../hql/internal/SemanticQueryBuilder.java | 156 ++++++++++-------- 2 files changed, 87 insertions(+), 73 deletions(-) diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 32aaf9bf8b89..5b1679c7d178 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -660,9 +660,7 @@ predicate | expression NOT? IN inList # InPredicate | expression NOT? BETWEEN expression AND expression # BetweenPredicate | expression NOT? (LIKE | ILIKE) expression likeEscape? # LikePredicate - | expression NOT? CONTAINS expression # ContainsPredicate - | expression NOT? INCLUDES expression # IncludesPredicate - | expression NOT? INTERSECTS expression # IntersectsPredicate + | expression NOT? (CONTAINS | INCLUDES | INTERSECTS) expression # BinaryExpressionPredicate | expression comparisonOperator expression # ComparisonPredicate | EXISTS collectionQuantifier LEFT_PAREN simplePath RIGHT_PAREN # ExistsCollectionPartPredicate | EXISTS expression # ExistsPredicate diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index c9723c4865a4..824b8942e34a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -32,6 +32,7 @@ import java.util.Set; import jakarta.persistence.criteria.Nulls; +import org.antlr.v4.runtime.Token; import org.hibernate.AssertionFailure; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; import org.hibernate.cfg.QuerySettings; @@ -2460,6 +2461,91 @@ public SqmPredicate visitUnaryIsPredicate(HqlParser.UnaryIsPredicateContext ctx) }; } + @Override + public SqmPredicate visitBinaryExpressionPredicate(HqlParser.BinaryExpressionPredicateContext ctx) { + final var firstSymbol = ((TerminalNode) ctx.getChild( 1 )).getSymbol(); + final boolean negated; + final Token operationSymbol; + if ( firstSymbol.getType() == HqlParser.NOT ) { + negated = true; + operationSymbol = ((TerminalNode) ctx.getChild( 2 )).getSymbol(); + } + else { + negated = false; + operationSymbol = firstSymbol; + } + final var expressions = ctx.expression(); + final var lhsCtx = expressions.get( 0 ); + final var rhsCtx = expressions.get( 1 ); + return switch ( operationSymbol.getType() ) { + case HqlParser.CONTAINS -> { + final var lhs = (SqmExpression) lhsCtx.accept( this ); + final var rhs = (SqmExpression) rhsCtx.accept( this ); + final var lhsExpressible = lhs.getExpressible(); + if ( lhsExpressible != null && !(lhsExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "First operand for contains predicate must be a basic plural type expression, but found: " + lhsExpressible.getSqmType(), + query + ); + } + final SelfRenderingSqmFunction contains = getFunctionDescriptor( + "array_contains" ).generateSqmExpression( + asList( lhs, rhs ), + null, + queryEngine() + ); + yield new SqmBooleanExpressionPredicate( contains, negated, nodeBuilder() ); + } + case HqlParser.INCLUDES -> { + final var lhs = (SqmExpression) lhsCtx.accept( this ); + final var rhs = (SqmExpression) rhsCtx.accept( this ); + final var lhsExpressible = lhs.getExpressible(); + final var rhsExpressible = rhs.getExpressible(); + if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "First operand for includes predicate must be a basic plural type expression, but found: " + + lhsExpressible.getSqmType(), + query + ); + } + if ( rhsExpressible != null && !( rhsExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "Second operand for includes predicate must be a basic plural type expression, but found: " + + rhsExpressible.getSqmType(), + query + ); + } + final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_includes" ).generateSqmExpression( + asList( lhs, rhs ), + null, + queryEngine() + ); + yield new SqmBooleanExpressionPredicate( contains, negated, nodeBuilder() ); + } + case HqlParser.INTERSECTS -> { + final var lhs = (SqmExpression) lhsCtx.accept( this ); + final var rhs = (SqmExpression) rhsCtx.accept( this ); + final var lhsExpressible = lhs.getExpressible(); + if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "First operand for intersects predicate must be a basic plural type expression, but found: " + + lhsExpressible.getSqmType(), + query + ); + } + final SelfRenderingSqmFunction contains = + getFunctionDescriptor( "array_intersects" ) + .generateSqmExpression( + asList( lhs, rhs ), + null, + queryEngine() + ); + yield new SqmBooleanExpressionPredicate( contains, negated, nodeBuilder() ); + } + default -> throw new AssertionError( "Unknown binary expression predicate: " + operationSymbol ); + }; + } + @Override public Object visitComparisonOperator(HqlParser.ComparisonOperatorContext ctx) { final TerminalNode firstToken = (TerminalNode) ctx.getChild( 0 ); @@ -2614,26 +2700,6 @@ private String getPossibleEnumValue(HqlParser.ExpressionContext expressionContex return null; } - @Override - public SqmPredicate visitContainsPredicate(HqlParser.ContainsPredicateContext ctx) { - final boolean negated = ctx.NOT() != null; - final SqmExpression lhs = (SqmExpression) ctx.expression( 0 ).accept( this ); - final SqmExpression rhs = (SqmExpression) ctx.expression( 1 ).accept( this ); - final SqmExpressible lhsExpressible = lhs.getExpressible(); - if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "First operand for contains predicate must be a basic plural type expression, but found: " + lhsExpressible.getSqmType(), - query - ); - } - final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_contains" ).generateSqmExpression( - asList( lhs, rhs ), - null, - queryEngine() - ); - return new SqmBooleanExpressionPredicate( contains, negated, nodeBuilder() ); - } - @Override public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContext ctx) { checkJsonFunctionsEnabled( ctx ); @@ -3168,56 +3234,6 @@ private void checkXmlFunctionsEnabled(ParserRuleContext ctx) { } } - @Override - public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { - final boolean negated = ctx.NOT() != null; - final SqmExpression lhs = (SqmExpression) ctx.expression( 0 ).accept( this ); - final SqmExpression rhs = (SqmExpression) ctx.expression( 1 ).accept( this ); - final SqmExpressible lhsExpressible = lhs.getExpressible(); - final SqmExpressible rhsExpressible = rhs.getExpressible(); - if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "First operand for includes predicate must be a basic plural type expression, but found: " - + lhsExpressible.getSqmType(), - query - ); - } - if ( rhsExpressible != null && !( rhsExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "Second operand for includes predicate must be a basic plural type expression, but found: " - + rhsExpressible.getSqmType(), - query - ); - } - final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_includes" ).generateSqmExpression( - asList( lhs, rhs ), - null, - queryEngine() - ); - return new SqmBooleanExpressionPredicate( contains, negated, nodeBuilder() ); - } - - @Override - public SqmPredicate visitIntersectsPredicate(HqlParser.IntersectsPredicateContext ctx) { - final boolean negated = ctx.NOT() != null; - final SqmExpression lhs = (SqmExpression) ctx.expression( 0 ).accept( this ); - final SqmExpression rhs = (SqmExpression) ctx.expression( 1 ).accept( this ); - final SqmExpressible lhsExpressible = lhs.getExpressible(); - if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "First operand for intersects predicate must be a basic plural type expression, but found: " - + lhsExpressible.getSqmType(), - query - ); - } - final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_intersects" ).generateSqmExpression( - asList( lhs, rhs ), - null, - queryEngine() - ); - return new SqmBooleanExpressionPredicate( contains, negated, nodeBuilder() ); - } - @Override public SqmPredicate visitLikePredicate(HqlParser.LikePredicateContext ctx) { final boolean negated = ctx.NOT() != null; From 2a41733ca05b3d26f298e654fa86848d1d4a8890 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 8 Sep 2025 14:48:37 +0200 Subject: [PATCH 7/9] HHH-19240 Fold ComparisonPredicate into BinaryExpressionPredicate rule (cherry picked from commit 5615bca4106b00157f2b29a2c2084809c9fa5469) --- .../org/hibernate/grammars/hql/HqlParser.g4 | 23 +++++-------- .../hql/internal/SemanticQueryBuilder.java | 34 +++++++------------ 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 5b1679c7d178..857387b37249 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -660,8 +660,15 @@ predicate | expression NOT? IN inList # InPredicate | expression NOT? BETWEEN expression AND expression # BetweenPredicate | expression NOT? (LIKE | ILIKE) expression likeEscape? # LikePredicate - | expression NOT? (CONTAINS | INCLUDES | INTERSECTS) expression # BinaryExpressionPredicate - | expression comparisonOperator expression # ComparisonPredicate + | expression + ( NOT? (CONTAINS | INCLUDES | INTERSECTS) + | EQUAL + | NOT_EQUAL + | GREATER + | GREATER_EQUAL + | LESS + | LESS_EQUAL + ) expression # BinaryExpressionPredicate | EXISTS collectionQuantifier LEFT_PAREN simplePath RIGHT_PAREN # ExistsCollectionPartPredicate | EXISTS expression # ExistsPredicate | NOT predicate # NegatedPredicate @@ -670,18 +677,6 @@ predicate | expression # BooleanExpressionPredicate ; -/** - * An operator which compares values for equality or order - */ -comparisonOperator - : EQUAL - | NOT_EQUAL - | GREATER - | GREATER_EQUAL - | LESS - | LESS_EQUAL - ; - /** * Any right operand of the 'in' operator * diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 824b8942e34a..8baef82183ff 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -2542,32 +2542,22 @@ public SqmPredicate visitBinaryExpressionPredicate(HqlParser.BinaryExpressionPre ); yield new SqmBooleanExpressionPredicate( contains, negated, nodeBuilder() ); } + case HqlParser.EQUAL -> + createComparisonPredicate( ComparisonOperator.EQUAL, lhsCtx, rhsCtx ); + case HqlParser.NOT_EQUAL -> + createComparisonPredicate( ComparisonOperator.NOT_EQUAL, lhsCtx, rhsCtx ); + case HqlParser.LESS -> + createComparisonPredicate( ComparisonOperator.LESS_THAN, lhsCtx, rhsCtx ); + case HqlParser.LESS_EQUAL -> + createComparisonPredicate( ComparisonOperator.LESS_THAN_OR_EQUAL, lhsCtx, rhsCtx ); + case HqlParser.GREATER -> + createComparisonPredicate( ComparisonOperator.GREATER_THAN, lhsCtx, rhsCtx ); + case HqlParser.GREATER_EQUAL -> + createComparisonPredicate( ComparisonOperator.GREATER_THAN_OR_EQUAL, lhsCtx, rhsCtx ); default -> throw new AssertionError( "Unknown binary expression predicate: " + operationSymbol ); }; } - @Override - public Object visitComparisonOperator(HqlParser.ComparisonOperatorContext ctx) { - final TerminalNode firstToken = (TerminalNode) ctx.getChild( 0 ); - return switch ( firstToken.getSymbol().getType() ) { - case HqlLexer.EQUAL -> ComparisonOperator.EQUAL; - case HqlLexer.NOT_EQUAL -> ComparisonOperator.NOT_EQUAL; - case HqlLexer.LESS -> ComparisonOperator.LESS_THAN; - case HqlLexer.LESS_EQUAL -> ComparisonOperator.LESS_THAN_OR_EQUAL; - case HqlLexer.GREATER -> ComparisonOperator.GREATER_THAN; - case HqlLexer.GREATER_EQUAL -> ComparisonOperator.GREATER_THAN_OR_EQUAL; - default -> throw new ParsingException( "Unrecognized comparison operator" ); - }; - } - - @Override - public SqmPredicate visitComparisonPredicate(HqlParser.ComparisonPredicateContext ctx) { - final ComparisonOperator comparisonOperator = (ComparisonOperator) ctx.comparisonOperator().accept( this ); - final var leftExpressionContext = ctx.expression( 0 ); - final var rightExpressionContext = ctx.expression( 1 ); - return createComparisonPredicate( comparisonOperator, leftExpressionContext, rightExpressionContext ); - } - @Override public SqmPredicate visitIsDistinctFromPredicate(HqlParser.IsDistinctFromPredicateContext ctx) { final var leftExpressionContext = ctx.expression( 0 ); From 2bc204eac7e7e2bca9782f54d5f04c96a6d41981 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 8 Sep 2025 14:52:07 +0200 Subject: [PATCH 8/9] HHH-19240 Fold IsDistinctFromPredicate into BinaryExpressionPredicate rule (cherry picked from commit ca0135a2f4ff4291743283d2473f6f1b703e2ae9) --- .../org/hibernate/grammars/hql/HqlParser.g4 | 2 +- .../hql/internal/SemanticQueryBuilder.java | 19 ++++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 857387b37249..364b3f33ac96 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -655,13 +655,13 @@ predicate //highest to lowest precedence : LEFT_PAREN predicate RIGHT_PAREN # GroupedPredicate | expression IS NOT? (NULL|EMPTY|TRUE|FALSE) # UnaryIsPredicate - | expression IS NOT? DISTINCT FROM expression # IsDistinctFromPredicate | expression NOT? MEMBER OF? path # MemberOfPredicate | expression NOT? IN inList # InPredicate | expression NOT? BETWEEN expression AND expression # BetweenPredicate | expression NOT? (LIKE | ILIKE) expression likeEscape? # LikePredicate | expression ( NOT? (CONTAINS | INCLUDES | INTERSECTS) + | IS NOT? DISTINCT FROM | EQUAL | NOT_EQUAL | GREATER diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 8baef82183ff..6a15148a85a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -2471,7 +2471,8 @@ public SqmPredicate visitBinaryExpressionPredicate(HqlParser.BinaryExpressionPre operationSymbol = ((TerminalNode) ctx.getChild( 2 )).getSymbol(); } else { - negated = false; + negated = firstSymbol.getType() == HqlParser.IS + && ((TerminalNode) ctx.getChild( 2 )).getSymbol().getType() == HqlParser.NOT; operationSymbol = firstSymbol; } final var expressions = ctx.expression(); @@ -2554,20 +2555,16 @@ public SqmPredicate visitBinaryExpressionPredicate(HqlParser.BinaryExpressionPre createComparisonPredicate( ComparisonOperator.GREATER_THAN, lhsCtx, rhsCtx ); case HqlParser.GREATER_EQUAL -> createComparisonPredicate( ComparisonOperator.GREATER_THAN_OR_EQUAL, lhsCtx, rhsCtx ); + case HqlParser.IS -> { + final ComparisonOperator comparisonOperator = !negated + ? ComparisonOperator.DISTINCT_FROM + : ComparisonOperator.NOT_DISTINCT_FROM; + yield createComparisonPredicate( comparisonOperator, lhsCtx, rhsCtx ); + } default -> throw new AssertionError( "Unknown binary expression predicate: " + operationSymbol ); }; } - @Override - public SqmPredicate visitIsDistinctFromPredicate(HqlParser.IsDistinctFromPredicateContext ctx) { - final var leftExpressionContext = ctx.expression( 0 ); - final var rightExpressionContext = ctx.expression( 1 ); - final ComparisonOperator comparisonOperator = ctx.NOT() == null - ? ComparisonOperator.DISTINCT_FROM - : ComparisonOperator.NOT_DISTINCT_FROM; - return createComparisonPredicate( comparisonOperator, leftExpressionContext, rightExpressionContext ); - } - private SqmComparisonPredicate createComparisonPredicate( ComparisonOperator comparisonOperator, HqlParser.ExpressionContext leftExpressionContext, From 55065be36eefac1a6a96537165c434d01f1d69bf Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 23 Sep 2025 19:33:24 +0200 Subject: [PATCH 9/9] HHH-19240 Improve memory consumption testing (cherry picked from commit 411bae9ee6d2dd5d7b380285fc4e3271694013be) --- .../test/hql/HqlParserMemoryUsageTest.java | 58 +---------- .../memory/GlobalMemoryUsageSnapshotter.java | 53 +++++++++++ ...HotspotPerThreadAllocationSnapshotter.java | 95 +++++++++++++++++++ .../HotspotTotalThreadBytesSnapshotter.java | 83 ++++++++++++++++ .../memory/MemoryAllocationSnapshot.java | 9 ++ .../memory/MemoryAllocationSnapshotter.java | 9 ++ .../testing/memory/MemoryUsageUtil.java | 27 ++++++ 7 files changed, 280 insertions(+), 54 deletions(-) create mode 100644 hibernate-testing/src/main/java/org/hibernate/testing/memory/GlobalMemoryUsageSnapshotter.java create mode 100644 hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotPerThreadAllocationSnapshotter.java create mode 100644 hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotTotalThreadBytesSnapshotter.java create mode 100644 hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshot.java create mode 100644 hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshotter.java create mode 100644 hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryUsageUtil.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java index c5bd4f3118a9..495a00eeba0d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java @@ -12,7 +12,7 @@ import jakarta.persistence.Table; import org.hibernate.cfg.QuerySettings; import org.hibernate.query.hql.HqlTranslator; -import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.testing.memory.MemoryUsageUtil; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.ServiceRegistry; @@ -80,65 +80,15 @@ SELECT AVG(p3.price) FROM Product p3 @Test public void testParserMemoryUsage(SessionFactoryScope scope) { final HqlTranslator hqlTranslator = scope.getSessionFactory().getQueryEngine().getHqlTranslator(); - final Runtime runtime = Runtime.getRuntime(); // Ensure classes and basic stuff is initialized in case this is the first test run hqlTranslator.translate( "from AppUser", AppUser.class ); - runtime.gc(); - runtime.gc(); - - // Track memory usage before execution - long totalMemoryBefore = runtime.totalMemory(); - long usedMemoryBefore = totalMemoryBefore - runtime.freeMemory(); - - System.out.println("Memory Usage Before Create Query:"); - System.out.println("----------------------------"); - System.out.println("Total Memory: " + (totalMemoryBefore / 1024) + " KB"); - System.out.println("Used Memory : " + (usedMemoryBefore / 1024) + " KB"); - System.out.println(); - - // Create query - SqmStatement statement = hqlTranslator.translate( HQL, Long.class ); - - // Track memory usage after execution - long totalMemoryAfter = runtime.totalMemory(); - long usedMemoryAfter = totalMemoryAfter - runtime.freeMemory(); - - System.out.println("Memory Usage After Create Query:"); - System.out.println("----------------------------"); - System.out.println("Total Memory: " + (totalMemoryAfter / 1024) + " KB"); - System.out.println("Used Memory : " + (usedMemoryAfter / 1024) + " KB"); - System.out.println(); - - System.out.println("Memory increase After Parsing:"); - System.out.println("----------------------------"); - System.out.println("Total Memory increase: " + ((totalMemoryAfter - totalMemoryBefore) / 1024) + " KB"); - System.out.println("Used Memory increase : " + ((usedMemoryAfter - usedMemoryBefore) / 1024) + " KB"); - System.out.println(); - - runtime.gc(); - runtime.gc(); - - // Track memory usage after execution - long totalMemoryAfterGc = runtime.totalMemory(); - long usedMemoryAfterGc = totalMemoryAfterGc - runtime.freeMemory(); - - System.out.println("Memory Usage After Create Query and GC:"); - System.out.println("----------------------------"); - System.out.println("Total Memory: " + (totalMemoryAfterGc / 1024) + " KB"); - System.out.println("Used Memory : " + (usedMemoryAfterGc / 1024) + " KB"); - System.out.println(); - - System.out.println("Memory overhead of Parsing:"); - System.out.println("----------------------------"); - System.out.println("Total Memory increase: " + ((totalMemoryAfter - totalMemoryAfterGc) / 1024) + " KB"); - System.out.println("Used Memory increase : " + ((usedMemoryAfter - usedMemoryAfterGc) / 1024) + " KB"); - System.out.println(); // During testing, before the fix for HHH-19240, the allocation was around 500+ MB, // and after the fix it dropped to 170 - 250 MB - final long memoryConsumption = usedMemoryAfter - usedMemoryAfterGc; - assertTrue( usedMemoryAfter - usedMemoryAfterGc < 256_000_000, "Parsing of queries consumes too much memory (" + ( memoryConsumption / 1024 ) + " KB), when at most 256 MB are expected" ); + final long memoryUsage = MemoryUsageUtil.estimateMemoryUsage( () -> hqlTranslator.translate( HQL, Long.class ) ); + System.out.println( "Memory Consumption: " + (memoryUsage / 1024) + " KB" ); + assertTrue( memoryUsage < 256_000_000, "Parsing of queries consumes too much memory (" + ( memoryUsage / 1024 ) + " KB), when at most 256 MB are expected" ); } @Entity(name = "Address") diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/GlobalMemoryUsageSnapshotter.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/GlobalMemoryUsageSnapshotter.java new file mode 100644 index 000000000000..055feea7bf30 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/GlobalMemoryUsageSnapshotter.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing.memory; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.util.List; + +final class GlobalMemoryUsageSnapshotter implements MemoryAllocationSnapshotter { + + private static final GlobalMemoryUsageSnapshotter INSTANCE = new GlobalMemoryUsageSnapshotter( + ManagementFactory.getMemoryPoolMXBeans() + ); + + private final List heapPoolBeans; + private final Runnable gcAndWait; + + private GlobalMemoryUsageSnapshotter(List heapPoolBeans) { + this.heapPoolBeans = heapPoolBeans; + this.gcAndWait = () -> { + for (int i = 0; i < 3; i++) { + System.gc(); + try { Thread.sleep(50); } catch (InterruptedException ignored) {} + } + }; + } + + public static GlobalMemoryUsageSnapshotter getInstance() { + return INSTANCE; + } + + @Override + public MemoryAllocationSnapshot snapshot() { + final long peakUsage = heapPoolBeans.stream().mapToLong(p -> p.getPeakUsage().getUsed()).sum(); + gcAndWait.run(); + final long retainedUsage = heapPoolBeans.stream().mapToLong(p -> p.getUsage().getUsed()).sum(); + heapPoolBeans.forEach(MemoryPoolMXBean::resetPeakUsage); + return new GlobalMemoryAllocationSnapshot( peakUsage, retainedUsage ); + } + + record GlobalMemoryAllocationSnapshot(long peakUsage, long retainedUsage) implements MemoryAllocationSnapshot { + + @Override + public long difference(MemoryAllocationSnapshot before) { + // When doing the "before" snapshot, the peak usage is reset. + // Since this object is the "after" snapshot, we can simply estimate the memory usage of an operation + // to be the peak usage of that operation minus the usage after GC + return peakUsage - retainedUsage; + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotPerThreadAllocationSnapshotter.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotPerThreadAllocationSnapshotter.java new file mode 100644 index 000000000000..e73d67e11fd8 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotPerThreadAllocationSnapshotter.java @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing.memory; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.lang.reflect.Method; +import java.util.HashMap; + +record HotspotPerThreadAllocationSnapshotter(ThreadMXBean threadMXBean) implements MemoryAllocationSnapshotter { + + private static final @Nullable HotspotPerThreadAllocationSnapshotter INSTANCE; + private static final Method GET_THREAD_ALLOCATED_BYTES; + + static { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + Method method = null; + try { + @SuppressWarnings("unchecked") + Class hotspotInterface = + (Class) Class.forName( "com.sun.management.ThreadMXBean" ); + try { + method = hotspotInterface.getMethod( "getThreadAllocatedBytes", long[].class ); + } + catch (Exception e) { + // Ignore + } + + if ( !hotspotInterface.isInstance( threadMXBean ) ) { + threadMXBean = ManagementFactory.getPlatformMXBean( hotspotInterface ); + } + } + catch (Throwable e) { + // Ignore + } + + GET_THREAD_ALLOCATED_BYTES = method; + + HotspotPerThreadAllocationSnapshotter instance = null; + if ( method != null && threadMXBean != null ) { + try { + instance = new HotspotPerThreadAllocationSnapshotter( threadMXBean ); + instance.snapshot(); + } + catch (Exception e) { + instance = null; + } + } + INSTANCE = instance; + } + + public static @Nullable HotspotPerThreadAllocationSnapshotter getInstance() { + return INSTANCE; + } + + @Override + public MemoryAllocationSnapshot snapshot() { + long[] threadIds = threadMXBean.getAllThreadIds(); + try { + return new PerThreadMemoryAllocationSnapshot( + threadIds, + (long[]) GET_THREAD_ALLOCATED_BYTES.invoke( threadMXBean, (Object) threadIds ) + ); + } + catch (Exception e) { + throw new RuntimeException( e ); + } + } + + record PerThreadMemoryAllocationSnapshot(long[] threadIds, long[] threadAllocatedBytes) + implements MemoryAllocationSnapshot { + + @Override + public long difference(MemoryAllocationSnapshot before) { + final PerThreadMemoryAllocationSnapshot other = (PerThreadMemoryAllocationSnapshot) before; + final HashMap previousThreadIdToIndexMap = new HashMap<>(); + for ( int i = 0; i < other.threadIds.length; i++ ) { + previousThreadIdToIndexMap.put( other.threadIds[i], i ); + } + long allocatedBytes = 0; + for ( int i = 0; i < threadIds.length; i++ ) { + allocatedBytes += threadAllocatedBytes[i]; + final Integer previousThreadIndex = previousThreadIdToIndexMap.get( threadIds[i] ); + if ( previousThreadIndex != null ) { + allocatedBytes -= other.threadAllocatedBytes[previousThreadIndex]; + } + } + return allocatedBytes; + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotTotalThreadBytesSnapshotter.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotTotalThreadBytesSnapshotter.java new file mode 100644 index 000000000000..d55c1e63f923 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotTotalThreadBytesSnapshotter.java @@ -0,0 +1,83 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing.memory; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.lang.reflect.Method; + +record HotspotTotalThreadBytesSnapshotter(ThreadMXBean threadMXBean) implements MemoryAllocationSnapshotter { + + private static final @Nullable HotspotTotalThreadBytesSnapshotter INSTANCE; + private static final Method GET_TOTAL_THREAD_ALLOCATED_BYTES; + + static { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + Method method = null; + try { + @SuppressWarnings("unchecked") + Class hotspotInterface = + (Class) Class.forName( "com.sun.management.ThreadMXBean" ); + try { + method = hotspotInterface.getMethod( "getTotalThreadAllocatedBytes" ); + } + catch (Exception e) { + // Ignore + } + + if ( !hotspotInterface.isInstance( threadMXBean ) ) { + threadMXBean = ManagementFactory.getPlatformMXBean( hotspotInterface ); + } + } + catch (Throwable e) { + // Ignore + } + + GET_TOTAL_THREAD_ALLOCATED_BYTES = method; + + HotspotTotalThreadBytesSnapshotter instance = null; + if ( method != null && threadMXBean != null ) { + try { + instance = new HotspotTotalThreadBytesSnapshotter( threadMXBean ); + instance.snapshot(); + } + catch (Exception e) { + instance = null; + } + } + INSTANCE = instance; + } + + public static @Nullable HotspotTotalThreadBytesSnapshotter getInstance() { + return INSTANCE; + } + + @Override + public MemoryAllocationSnapshot snapshot() { + try { + return new GlobalMemoryAllocationSnapshot( (long) GET_TOTAL_THREAD_ALLOCATED_BYTES.invoke( threadMXBean ) ); + } + catch (Exception e) { + throw new RuntimeException( e ); + } + } + + record GlobalMemoryAllocationSnapshot(long allocatedBytes) implements MemoryAllocationSnapshot { + + GlobalMemoryAllocationSnapshot { + if ( allocatedBytes == -1L ) { + throw new IllegalArgumentException( "getTotalThreadAllocatedBytes is disabled" ); + } + } + + @Override + public long difference(MemoryAllocationSnapshot before) { + final GlobalMemoryAllocationSnapshot other = (GlobalMemoryAllocationSnapshot) before; + return Math.max( allocatedBytes - other.allocatedBytes, 0L ); + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshot.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshot.java new file mode 100644 index 000000000000..1aba66cef8db --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshot.java @@ -0,0 +1,9 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing.memory; + +interface MemoryAllocationSnapshot { + long difference(MemoryAllocationSnapshot before); +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshotter.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshotter.java new file mode 100644 index 000000000000..75d87fc9325f --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshotter.java @@ -0,0 +1,9 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing.memory; + +interface MemoryAllocationSnapshotter { + MemoryAllocationSnapshot snapshot(); +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryUsageUtil.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryUsageUtil.java new file mode 100644 index 000000000000..55d458832bc6 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryUsageUtil.java @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing.memory; + +public class MemoryUsageUtil { + + private static final MemoryAllocationSnapshotter SNAPSHOTTER; + + static { + MemoryAllocationSnapshotter snapshotter = HotspotTotalThreadBytesSnapshotter.getInstance(); + if ( snapshotter == null ) { + snapshotter = HotspotPerThreadAllocationSnapshotter.getInstance(); + } + if ( snapshotter == null ) { + snapshotter = GlobalMemoryUsageSnapshotter.getInstance(); + } + SNAPSHOTTER = snapshotter; + } + + public static long estimateMemoryUsage(Runnable runnable) { + final MemoryAllocationSnapshot beforeSnapshot = SNAPSHOTTER.snapshot(); + runnable.run(); + return SNAPSHOTTER.snapshot().difference( beforeSnapshot ); + } +}