Repository navigation
Allow sharing immutable listener lists across Hosts, VMs and Cloudlets to reduce memory use #607
Description
Idea
Large simulations may create millions of Cloudlets and thousands of VMs and Hosts. When the same listener is added to all of them (such as in a loop or by a builder), each object creates its own list with that listener. This issue proposes letting users pass a list of listeners that is shared by many objects, so each object doesn't need its own list.
Sharing is a user decision: the framework never shares lists on its own. The builders and the examples will use the new methods, so users see it as the best practice to reduce memory use.
Current state
Since #586 (PR #593), listener lists of HostAbstract, VmAbstract, CloudletAbstract, DatacenterBrokerAbstract and CloudSimPlus start with the shared empty List.of() and are replaced by a new immutable list on each change (see the Listeners helper class). So an object without listeners only pays for its list fields (4 B each).
But when a listener is added, each object creates its own list, even if all objects get the same listener:
for (int i = 0; i < CLOUDLETS; i++) {
// Each Cloudlet creates its own List.of(listener) (24 B).
// this::onCloudletFinish also creates a new listener object in each iteration (16 to 24 B).
cloudletList.add(CloudletSimple.of(...).addOnFinishListener(this::onCloudletFinish));
}Sizes were measured with JOL 0.17 (Java Object Layout, an OpenJDK tool that shows how the JVM lays out the fields of an object and how many bytes it uses) on JDK 25 (64-bit, compressed oops: 12 B object header, 4 B references, 8 B alignment), using the current master (447875b). A list with 1 or 2 listeners (List.of(a)/List.of(a, b)) uses 24 B. A lambda or method reference that captures 1 reference (such as this) uses 16 B, and 24 B if it captures 2 or 3 references.
The table shows the memory each object uses to store listeners: its list fields, plus the list when there are listeners.
| Class | No listeners | 1 listener: own list (today) | 1 listener: shared list (proposal) | Listener lists (bytes of the fields) |
|---|---|---|---|---|
| Host | 12 B | 36 B | 12 B (−24 B) | 3 (12 B) |
| VM | 24 B | 48 B | 24 B (−24 B) | 6 (24 B) |
| Cloudlet | 12 B | 36 B | 12 B (−24 B) | 3 (12 B) |
If the loop also creates a new listener object in each iteration (e.g. this::onCloudletFinish inside the loop), each object uses 16 to 24 B more today. Creating the shared list once, outside the loop, removes that cost too.
At scale, for 1M Cloudlets with the same finish listener:
| Code | Memory to store listeners |
|---|---|
Today, with this::onCloudletFinish inside the loop |
about 52 to 60 MB |
| Today, with the same listener instance | about 36 MB |
| Proposal, with a shared list | about 12 MB (just the fields, as for Cloudlets without listeners) |
Proposal
New methods
Add one method for each existing addOn...Listener in Host, Vm and Cloudlet, with a plural name, receiving a list of listeners:
// Cloudlet
Cloudlet addOnStartListeners(List<EventListener<CloudletVmEventInfo>> listeners);
Cloudlet addOnFinishListeners(List<EventListener<CloudletVmEventInfo>> listeners);
Cloudlet addOnUpdateProcessingListeners(List<EventListener<CloudletVmEventInfo>> listeners);
// Vm: the same for its 6 event types (migration start/finish, host placement/removal, update processing, creation failure)
// Host: the same for its 3 event types (update processing, startup, shutdown)Their docs say:
Adds a list of listeners that can be shared by many objects, to reduce memory use.
Create the list once withList.of(...)and pass the same list to all objects (e.g. inside a loop).
If this object has no listeners of this type, it stores the given list itself, so it uses no memory for its own list.
Changes later made in one object don't affect the others.
Why it's safe (listeners never go to the wrong object)
- The stored list is immutable. Adding or removing a listener on one object creates a new list just for that object (as
Listenersalready does), and the other objects keep the shared list. - If the user passes a mutable list (e.g. an
ArrayList), it's copied withList.copyOf(). So later changes to that list don't affect any object.List.copyOf()returns the same instance for lists that are already immutable (created withList.of()orList.copyOf()), so they are not copied. - There is no global state (such as a static cache), so simulations running in parallel never share anything.
- Notification doesn't change: the field is read once and the list is immutable, so the semantics for listeners added or removed during notification stay the same (see
testOnHostPlacementListenerAddedDuringNotificationIsCalledOnlyInNextRoundandtestOnUpdateProcessingListenerAddedDuringNotificationIsCalledOnlyInNextRound).CloudletAbstract.onFinishstill clears its ownonFinishListenersfield before notifying, which doesn't affect other objects sharing the list.
Implementation
A new method in Listeners:
/// Adds all listeners from a list that may be shared by many objects.
/// If the current list is empty, returns the given list itself (or an immutable copy, if it's mutable),
/// so all objects receiving the same list share it.
/// @param listeners the current immutable list of listeners
/// @param shared the listeners to add, usually created once with `List.of(...)`
/// @param <T> the type of the listeners
/// @return the list with all listeners
public static <T> List<T> addAll(@NonNull final List<T> listeners, @NonNull final List<? extends T> shared) {
// Doesn't copy lists created with List.of() or List.copyOf(), since they are already immutable
final List<T> immutable = List.copyOf(shared);
if (listeners.isEmpty()) {
return immutable;
}
var result = listeners;
for (final T listener : immutable) {
result = add(result, listener);
}
return result;
}Each owner class just delegates to it:
@Override
public Cloudlet addOnFinishListeners(@NonNull final List<EventListener<CloudletVmEventInfo>> listeners) {
onFinishListeners = Listeners.addAll(onFinishListeners, listeners);
return this;
}HostNull, VmNull and CloudletNull implement the new methods doing nothing.
Builders
CloudletBuilder, VmBuilder and HostBuilder already keep the listener in a field and add the same instance to each object they create, but each object still creates its own list. They will create the list once in the setter and pass it to each object:
private List<EventListener<CloudletVmEventInfo>> onCloudletFinishListeners = List.of();
public CloudletBuilder setOnCloudletFinishListener(@NonNull final EventListener<CloudletVmEventInfo> listener) {
this.onCloudletFinishListeners = EventListener.NULL.equals(listener) ? List.of() : List.of(listener);
return this;
}
// when creating each Cloudlet
cloudlet.addOnFinishListeners(onCloudletFinishListeners);This also fixes a waste in VmBuilder: today it always calls addOnHostPlacementListener(EventListener.NULL) by default, and since that method doesn't ignore EventListener.NULL, each VM created by the builder keeps a 1-element list (24 B) with a listener that does nothing.
Examples
The examples that add the same listener to each object in a loop (in cloudsimplus-examples) will create the list once and use the new methods:
// Created once, outside the loop
final List<EventListener<CloudletVmEventInfo>> finishListeners = List.of(this::onCloudletFinishListener);
for (int i = 0; i < CLOUDLETS; i++) {
cloudletList.add(CloudletSimple.of(...).addOnFinishListeners(finishListeners));
}Examples to update:
MigrationExample1(migration start and finish),ManualMigrationExample1andInterDatacenterMigration1(migration start);CloudletListenersExample1(start and finish) andCloudletListenersExample2_ResourceUsageAlongTime(update processing);CreateCloudletAfterLastFinishedOne(finish);VmListenersExample2(host placement and removal: it already reuses the same listener instance, but each VM still creates its own list).
Examples that add a listener to a single object (e.g. vmList.getFirst()) stay as they are.
Listeners that need per-object state (e.g. a lambda that captures the object or a loop variable) can't be shared, so they keep using the single-listener methods. The same applies to VM scaling: VmAbstract adds vmScaling::requestUpScalingIfPredicateMatches, which is bound to each VM's own scaling object.
Related fixes
These can be separate small PRs:
-
ArrayList→List.of():CloudletSchedulerAbstractandDatacenterSimplestill create anArrayListfor each listener list in the constructor. UsingList.of()and theListenershelper, like the classes changed in Listener lists: lazy creation and safe changes during notification #586:Class No listeners: today → after 1 listener: today → after Listener lists (bytes of the fields) CloudletScheduler (one per VM) 28 → 4 B (−24 B) 84 → 28 B (−56 B) 1 (4 B) Datacenter 56 → 8 B (−48 B) 112 → 32 B (−80 B) 2 (8 B) An
ArrayListuses 24 B when empty and 80 B with 1 listener (it creates a 10-slot array). For 100K VMs, the scheduler change saves about 2.4 MB. InDatacenterSimple, it also fixes a bug risk:forEachover anArrayListthrowsConcurrentModificationExceptionif a listener removes itself while being notified.CloudletSchedulerAbstractnotifies listeners with a reversed indexed loop, so after the change they'll be notified in the order they were added (small behaviour change). -
Ignore
EventListener.NULLin every add method. Today these methods don't ignore it:VmAbstract.addOnHostPlacementListener,addOnMigrationStartListenerandaddOnMigrationFinishListener,CloudletAbstract.addOnStartListenerandaddOnUpdateProcessingListener,DatacenterSimple.addOnHostAvailableListenerandaddOnVmMigrationFinishListener,DatacenterBrokerAbstract.addOnVmsCreatedListenerand theCloudSimPlusadd methods. -
Fix the
Listenersclass docs: aList.of()with 1 listener uses 24 B (measured with JOL), not 16 B.
DatacenterBrokerAbstract (1 list) and CloudSimPlus (4 lists, one object per simulation) don't need changes.
Options considered and dropped
| Option | Why it was dropped |
|---|---|
Group the lists of each class into a holder record (HostListeners, VmListeners, CloudletListeners), with a shared NULL holder |
Saves 8 B (Host, Cloudlet) or 16 B (VM) per object without listeners, but uses 16 to 24 B more per object with listeners, since the holder repeats one slot per event type plus its own header. Using null instead of a NULL holder doesn't change that: a field takes 4 B either way. |
One List<Object> per object with pairs of event type and listener |
Uses less memory in both cases, but each notification must scan all listeners of the object to find the ones of a type, and it needs unchecked casts. The code is harder to read. |
| A map from event type to listeners | Map.of() with 1 key uses 32 B, EnumMap 40 B plus an array, HashMap 48 B plus a table and nodes. All use more memory than today when there are listeners. |
| A global cache that makes objects receiving the same listener share a list automatically | Saves the same memory as this proposal without user changes, but needs a static cache shared by simulations running in parallel (thread safety, and keeping listeners of finished simulations alive). Sharing should be a visible user decision. |
Time complexity
- Notification doesn't change, so the processing update chain (Datacenter → Host → Vm → CloudletScheduler) has no extra cost.
addOn...Listenerswith a list created byList.of()is O(1) when the object has no listeners of that type (List.copyOf()only checks the list type). When it already has listeners, it's O(n × m) on the number of listeners (usually 1 to 3).
Compatibility
- The new methods are added to sealed interfaces (
Host,Vm,Cloudlet), so it's not a breaking change. - The single-listener methods are kept.
- Builder setters keep their signatures.
Implementation plan
- Do the related fixes 1 and 2 (separate small PRs), with tests.
- Add
Listeners.addAll(). - Add the
addOn...Listenersmethods toCloudlet,VmandHost, their*Abstractclasses and their*Nullclasses. - Update
CloudletBuilder,VmBuilderandHostBuilderto create the lists once and use the new methods. - Add tests:
- two objects receiving the same list keep the same instance (
assertSame); - adding or removing a listener on one object doesn't change the other;
- a mutable list is copied, so changing it later doesn't affect the objects;
- an object that already has listeners gets the shared ones added, without duplicates;
- one test for each
addOn...Listenersmethod, to keep it in sync with its single-listener method; - the existing tests about listeners added or removed during notification keep passing.
- two objects receiving the same list keep the same instance (
- Update the examples listed above and fix the
Listenersdocs. - Measure the heap of a large example (e.g. 1M Cloudlets with a finish listener) before and after.
🤖 Generated with Claude Code
Activity
[-]Group Host, Vm and Cloudlet listener lists into a lazy holder object (HostListeners, VmListeners, CloudletListeners)[/-][+]Allow sharing immutable listener lists across Hosts, VMs and Cloudlets to reduce memory use[/+]