-
Bug
-
Resolution: Unresolved
-
Minor
-
None
Due to a job-dsl script that runs every 5 minutes creating thousands of "temporary" class loaders, pipelines builds take longer and longer to complete, though they complete all tasks.
Turning on FINEST logging for CpsFlowExecution showed these types of entries repeated endlessly:
Sep 19, 2024 6:09:30 AM FINEST org.jenkinsci.plugins.workflow.cps.CpsFlowExecution
ignoring class sfdc.factory.AppJobFactory$_addStopAllApps_closure16$_closure99 with loader groovy.lang.GroovyClassLoader$InnerLoader@19b1f70
Sep 19, 2024 6:09:30 AM FINEST org.jenkinsci.plugins.workflow.cps.CpsFlowExecution
ignoring class groovy.tmp.templates.StreamingTemplateScript324068 with loader groovy.lang.GroovyClassLoader$InnerLoader@7164c9ed
Sep 19, 2024 6:09:30 AM FINEST org.jenkinsci.plugins.workflow.cps.CpsFlowExecution
ignoring class groovy.tmp.templates.StreamingTemplateScript229000$_getTemplate_closure1$_closure3 with loader groovy.lang.GroovyClassLoader$InnerLoader@38a504f0
I'm encountering this on release 3867.v535458ce43fd, though it seems that the latest code is going to behave in more or less the same way.
------------------
This script gives an idea of the delay interval added to the end of every pipeline build by just iterating on the classloaders (and printing the count).
import java.lang.ref.WeakReferencedef timeIterationOfGlobalClassValues(ClassLoader loader) {
try {
// Get the ClassInfo class
def classInfoC = Class.forName("org.codehaus.groovy.reflection.ClassInfo")// Access the globalClassValue field
def globalClassValueF = classInfoC.getDeclaredField("globalClassValue")
globalClassValueF.setAccessible(true)
def globalClassValue = globalClassValueF.get(null)// Check if GroovyClassValuePreJava7 is in use
def groovyClassValuePreJava7C = Class.forName("org.codehaus.groovy.reflection.GroovyClassValuePreJava7")
if (!groovyClassValuePreJava7C.isInstance(globalClassValue)){{{}
{}}}// Access the map field
def mapF = groovyClassValuePreJava7C.getDeclaredField("map")
mapF.setAccessible(true)
def map = mapF.get(globalClassValue)// Get map entries
def groovyClassValuePreJava7Map = Class.forName("org.codehaus.groovy.reflection.GroovyClassValuePreJava7\$GroovyClassValuePreJava7Map")
def entries = groovyClassValuePreJava7Map.getMethod("values").invoke(map)
def removeM = groovyClassValuePreJava7Map.getMethod("remove", Object.class)// Get value from entries
def entryC = Class.forName("org.codehaus.groovy.util.AbstractConcurrentMapBase\$Entry")
def getValueM = entryC.getMethod("getValue")// To store classes to remove
def toRemove = []
def ignoredClassLoaderCount = 0 // Counter for ignored class loaders// Start timing the iteration
def startTime = System.nanoTime()try {
// For Groovy 2.4.8+
def classRefF = classInfoC.getDeclaredField("classRef")
classRefF.setAccessible(true)
for (entry in entries){{{}
{}}}} catch (NoSuchFieldException e) {
// For Groovy 2.4.7 and below
def klazzF = classInfoC.getDeclaredField("klazz")
klazzF.setAccessible(true)
for (entry in entries){{{}
{}}}}// Iterate and remove classes not associated with the provided loader
def it = toRemove.iterator()
while (it.hasNext()) {
def clazz = it.next()
try {
def encounteredLoader = clazz.getClassLoader()
if (encounteredLoader != loader){{{}
{}}}} catch (Error e) {
println("Error: ${e}")
e.printStackTrace()
}
}// End timing
def endTime = System.nanoTime()// Calculate the elapsed time in milliseconds
def elapsedTimeInMs = (endTime - startTime) / 1_000_000
def minutes = (elapsedTimeInMs / 60000) as int
def seconds = ((elapsedTimeInMs.longValue() % 60000) as int) / 1000println("Total time for iteration: ${minutes} minute(s) and ${seconds} second(s)")
println("Total ignored class loaders: ${ignoredClassLoaderCount}")
} catch (Exception e) {
println("Error: ${e}")
e.printStackTrace()
}
}// Example usage in Jenkins Script Console
def loader = Jenkins.instance.pluginManager.uberClassLoader
timeIterationOfGlobalClassValues(loader)