@@ -346,6 +346,143 @@ public void testMethodMapCleanupDuringContinuousProfile() throws Exception {
346346 + "Classes allowed to unload naturally for optimal memory usage" );
347347 }
348348
349+ /**
350+ * Comparison test that validates cleanup effectiveness by running the same workload
351+ * twice: once without cleanup (no-method-cleanup) and once with cleanup (default).
352+ * This provides empirical evidence that the cleanup mechanism actually prevents
353+ * unbounded growth.
354+ */
355+ @ Test
356+ public void testCleanupEffectivenessComparison () throws Exception {
357+ // Verify NMT is enabled
358+ Assumptions .assumeTrue (
359+ NativeMemoryTracking .isEnabled (), "Test requires -XX:NativeMemoryTracking=detail" );
360+
361+ // Stop the default profiler from AbstractProfilerTest
362+ // We need to manage our own profiler instances for this comparison
363+ stopProfiler ();
364+
365+ final int iterations = 8 ; // Fewer iterations but enough to see difference
366+ final int classesPerIteration = 50 ; // 250 methods per iteration
367+
368+ // ===== Phase 1: WITHOUT cleanup (no-method-cleanup) =====
369+ System .out .println ("\n === Phase 1: WITHOUT cleanup (no-method-cleanup) ===" );
370+
371+ NativeMemoryTracking .NMTSnapshot beforeNoCleanup = NativeMemoryTracking .takeSnapshot ();
372+
373+ Path noCleanupFile = tempFile ("no-cleanup" );
374+ profiler .execute (
375+ "start," + getProfilerCommand () + ",jfr,no-method-cleanup,file=" + noCleanupFile );
376+
377+ Thread .sleep (500 ); // Stabilize
378+ NativeMemoryTracking .NMTSnapshot afterStartNoCleanup =
379+ NativeMemoryTracking .takeSnapshot ();
380+
381+ // Run workload without cleanup
382+ for (int iter = 0 ; iter < iterations ; iter ++) {
383+ for (int i = 0 ; i < classesPerIteration ; i ++) {
384+ Class <?>[] transientClasses = generateUniqueMethodCalls ();
385+ for (Class <?> clazz : transientClasses ) {
386+ invokeClassMethods (clazz );
387+ }
388+ }
389+
390+ profiler .dump (noCleanupFile );
391+ Thread .sleep (100 );
392+
393+ if ((iter + 1 ) % 3 == 0 ) {
394+ System .gc ();
395+ Thread .sleep (50 );
396+ }
397+ }
398+
399+ NativeMemoryTracking .NMTSnapshot afterNoCleanup = NativeMemoryTracking .takeSnapshot ();
400+ long growthNoCleanup =
401+ afterNoCleanup .internalReservedKB - afterStartNoCleanup .internalReservedKB ;
402+ System .out .println (
403+ String .format (
404+ "WITHOUT cleanup: Internal memory growth = +%d KB\n "
405+ + "Check TEST_LOG: MethodMap should grow unbounded (no cleanup logs)" ,
406+ growthNoCleanup ));
407+
408+ profiler .stop ();
409+ Thread .sleep (500 ); // Allow cleanup
410+
411+ // ===== Phase 2: WITH cleanup (default) =====
412+ System .out .println ("\n === Phase 2: WITH cleanup (default) ===" );
413+
414+ // Reset class counter to generate same classes
415+ classCounter = 0 ;
416+
417+ NativeMemoryTracking .NMTSnapshot beforeWithCleanup = NativeMemoryTracking .takeSnapshot ();
418+
419+ Path withCleanupFile = tempFile ("with-cleanup" );
420+ profiler .execute (
421+ "start," + getProfilerCommand () + ",jfr,method-cleanup,file=" + withCleanupFile );
422+
423+ Thread .sleep (500 ); // Stabilize
424+ NativeMemoryTracking .NMTSnapshot afterStartWithCleanup =
425+ NativeMemoryTracking .takeSnapshot ();
426+
427+ // Run same workload with cleanup
428+ for (int iter = 0 ; iter < iterations ; iter ++) {
429+ for (int i = 0 ; i < classesPerIteration ; i ++) {
430+ Class <?>[] transientClasses = generateUniqueMethodCalls ();
431+ for (Class <?> clazz : transientClasses ) {
432+ invokeClassMethods (clazz );
433+ }
434+ }
435+
436+ profiler .dump (withCleanupFile );
437+ Thread .sleep (100 );
438+
439+ if ((iter + 1 ) % 3 == 0 ) {
440+ System .gc ();
441+ Thread .sleep (50 );
442+ }
443+ }
444+
445+ NativeMemoryTracking .NMTSnapshot afterWithCleanup = NativeMemoryTracking .takeSnapshot ();
446+ long growthWithCleanup =
447+ afterWithCleanup .internalReservedKB - afterStartWithCleanup .internalReservedKB ;
448+ System .out .println (
449+ String .format (
450+ "WITH cleanup: Internal memory growth = +%d KB\n "
451+ + "Check TEST_LOG: MethodMap should stay bounded, cleanup logs should appear" ,
452+ growthWithCleanup ));
453+
454+ profiler .stop ();
455+
456+ // ===== Comparison =====
457+ System .out .println ("\n === Comparison ===" );
458+ System .out .println (
459+ String .format (
460+ "WITHOUT cleanup: +%d KB\n " + "WITH cleanup: +%d KB\n " + "Savings: %d KB (%.1f%%)" ,
461+ growthNoCleanup ,
462+ growthWithCleanup ,
463+ growthNoCleanup - growthWithCleanup ,
464+ 100.0 * (growthNoCleanup - growthWithCleanup ) / growthNoCleanup ));
465+
466+ // Assert that cleanup actually reduces memory growth
467+ // We expect at least 20% savings from cleanup
468+ long expectedMinSavings = (long ) (growthNoCleanup * 0.20 );
469+ long actualSavings = growthNoCleanup - growthWithCleanup ;
470+
471+ if (actualSavings < expectedMinSavings ) {
472+ fail (
473+ String .format (
474+ "Cleanup not effective enough!\n "
475+ + "Expected at least 20%% savings (>= %d KB)\n "
476+ + "Actual savings: %d KB (%.1f%%)\n "
477+ + "This suggests cleanup is not working as intended" ,
478+ expectedMinSavings ,
479+ actualSavings ,
480+ 100.0 * actualSavings / growthNoCleanup ));
481+ }
482+
483+ System .out .println ("Result: Cleanup effectiveness validated - significant memory savings observed" );
484+ }
485+
349486 private Path tempFile (String name ) throws IOException {
350487 Path dir = Paths .get ("/tmp/recordings" );
351488 Files .createDirectories (dir );
0 commit comments