diff --git a/core/src/main/java/lucee/transformer/bytecode/PageImpl.java b/core/src/main/java/lucee/transformer/bytecode/PageImpl.java index 916089866c5..df7324fae09 100755 --- a/core/src/main/java/lucee/transformer/bytecode/PageImpl.java +++ b/core/src/main/java/lucee/transformer/bytecode/PageImpl.java @@ -118,6 +118,9 @@ public final class PageImpl extends BodyBase implements Page { // Maximum UDFs per constructor helper method to avoid JVM's 64KB method bytecode limit, chosen value is a safe estimate, size depends on CFC methdod signatures private static final int MAX_UDF_PER_CONSTRUCTOR_METHOD = 30; + // Maximum keys per helper method to avoid JVM's 64KB method bytecode limit, each key generates ~30 bytes + private static final int MAX_KEYS_PER_CINIT_METHOD = 1000; + public static final Type NULL = Type.getType(lucee.runtime.type.Null.class); public static final Type KEY_IMPL = Type.getType(KeyImpl.class); public static final Type KEY_CONSTANTS = Type.getType(KeyConstants.class); @@ -1394,16 +1397,43 @@ else if (defaultExpr instanceof LitBooleanImpl) { ga.push(keys.size()); // Array size ga.newArray(Types.COLLECTION_KEY); - int index = 0; - for (LitString ls: keys.keySet()) { - ga.dup(); - ga.push(index++); - ga.push(ls.getString()); + // Split into helper methods if there are enough keys to risk MethodTooLargeException (LDEV-6134) + List keyList = new ArrayList<>(keys.keySet()); + + if (keyList.size() > MAX_KEYS_PER_CINIT_METHOD) { + int batchNum = 0; + for (int batchStart = 0; batchStart < keyList.size(); batchStart += MAX_KEYS_PER_CINIT_METHOD) { + int batchEnd = Math.min(batchStart + MAX_KEYS_PER_CINIT_METHOD, keyList.size()); + String helperMethodName = ASMUtil.createOverfowMethod("_cinitKeys", batchNum++); + Method helperMethod = new Method(helperMethodName, Type.VOID_TYPE, new Type[] { Types.COLLECTION_KEY_ARRAY }); + GeneratorAdapter helperAdapter = new GeneratorAdapter( + Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC + Opcodes.ACC_SYNTHETIC, helperMethod, null, null, cw); + + // call helper from + ga.dup(); + ga.invokeStatic(Type.getObjectType(name), helperMethod); - // ExpressionUtil.writeOutSilent(ls, bc, Expression.MODE_REF); - ga.invokeStatic(KEY_IMPL, KEY_INIT_KEYS); - ga.arrayStore(Types.COLLECTION_KEY); + // write batch into helper method + writeKeysBatch(helperAdapter, keyList, batchStart, batchEnd); + + helperAdapter.returnValue(); + helperAdapter.endMethod(); + } } + else { + // Small number of keys, inline directly in + int index = 0; + for (LitString ls: keyList) { + ga.dup(); + ga.push(index++); + ga.push(ls.getString()); + + // ExpressionUtil.writeOutSilent(ls, bc, Expression.MODE_REF); + ga.invokeStatic(KEY_IMPL, KEY_INIT_KEYS); + ga.arrayStore(Types.COLLECTION_KEY); + } + } + ga.putStatic(Type.getObjectType(name), "keys", Types.COLLECTION_KEY_ARRAY); ga.returnValue(); @@ -1521,6 +1551,18 @@ private String getTagAttributeValue(Tag tag, String attrName) { return null; } + private static void writeKeysBatch(GeneratorAdapter ga, List keyList, int from, int to) { + for (int i = from; i < to; i++) { + ga.loadArg(0); // CollectionKey[] array + ga.push(i); + ga.push(keyList.get(i).getString()); + + // ExpressionUtil.writeOutSilent(ls, bc, Expression.MODE_REF); + ga.invokeStatic(KEY_IMPL, KEY_INIT_KEYS); + ga.arrayStore(Types.COLLECTION_KEY); + } + } + private void writeOutStaticConstructor(ConstrBytecodeContext constr, Map keys, ClassWriter cw, TagCIObject component, String name) throws TransformerException { diff --git a/test/tickets/LDEV6134.cfc b/test/tickets/LDEV6134.cfc new file mode 100644 index 00000000000..0cd64b5906d --- /dev/null +++ b/test/tickets/LDEV6134.cfc @@ -0,0 +1,50 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="bytecode" { + + function beforeAll() { + generateFixture(); + } + + function run( testResults, testBox ) { + describe( "LDEV-6134 - MethodTooLargeException in when CFC has thousands of unique string keys", function() { + it( title="component with 3000 keys compiles and instantiates without MethodTooLargeException", body=function( currentSpec ) { + var obj = new LDEV6134.CfcWithExcessStaticObjectKeys( parent={} ); + expect( obj ).notToBeNull(); + expect( obj.testWord( "word_000001" ) ).toBe( "word_000001" ); + }); + }); + } + + private string function getFixtureDir() { + return getDirectoryFromPath( getCurrentTemplatePath() ) & "LDEV6134/"; + } + + private string function getFixturePath() { + return getFixtureDir() & "CfcWithExcessStaticObjectKeys.cfc"; + } + + private void function generateFixture() { + var dir = getFixtureDir(); + if ( !directoryExists( dir ) ) directoryCreate( dir ); + var path = getFixturePath(); + // cleanup any stale fixture from a previous run + if ( fileExists( path ) ) fileDelete( path ); + + var keyCount = 3000; + var nl = chr( 10 ); + var cfml = 'component {#nl##nl#'; + cfml &= ' function init( parent ) {#nl#'; + cfml &= ' return this;#nl#'; + cfml &= ' }#nl##nl#'; + cfml &= ' string function testWord( required string word ) {#nl#'; + cfml &= ' var words = {};#nl#'; + loop from="1" to="#keyCount#" index="local.i" { + var key = "word_#numberFormat( i, '000000' )#"; + cfml &= ' words["#key#"] = "#key#";#nl#'; + } + cfml &= ' return words[ARGUMENTS.word];#nl#'; + cfml &= ' }#nl#'; + cfml &= '}#nl#'; + fileWrite( path, cfml ); + } + +} diff --git a/test/tickets/LDEV6134/.gitignore b/test/tickets/LDEV6134/.gitignore new file mode 100644 index 00000000000..5fe35dcc56d --- /dev/null +++ b/test/tickets/LDEV6134/.gitignore @@ -0,0 +1 @@ +CfcWithExcessStaticObjectKeys.cfc