diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index d10d16a..11dc57a 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -17,5 +17,14 @@ jobs: distribution: 'corretto' java-version: 25 cache: 'maven' + - name: Install CloudSim Plus before cloudsimplus#595 + # The benchmarks use the CloudSim Plus API before cloudsimplus#595 (a List per Host), + # which isn't published. So it's built from the commit right before that PR. + # Remove this step (and -nsu below) when the benchmarks use the API after #595. + run: | + git clone --quiet --filter=blob:none https://github.com/cloudsimplus/cloudsimplus.git "$RUNNER_TEMP/cloudsimplus" + git -C "$RUNNER_TEMP/cloudsimplus" checkout --quiet 9bb9de25c + mvn -B -q -f "$RUNNER_TEMP/cloudsimplus/pom.xml" install -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip - name: Maven Build - run: mvn -B package + # -nsu (no snapshot updates) avoids replacing the CloudSim Plus installed above by the published 9.0.0-SNAPSHOT. + run: mvn -B -nsu package diff --git a/src/main/java/org/cloudsimplus/benchmarks/HeapMeter.java b/src/main/java/org/cloudsimplus/benchmarks/HeapMeter.java new file mode 100644 index 0000000..75510a0 --- /dev/null +++ b/src/main/java/org/cloudsimplus/benchmarks/HeapMeter.java @@ -0,0 +1,108 @@ +/* + * CloudSim Plus: A modern, highly-extensible and easier-to-use Framework for + * Modeling and Simulation of Cloud Computing Infrastructures and Services. + * http://cloudsimplus.org + * + * Copyright (C) 2015-2021 Universidade da Beira Interior (UBI, Portugal) and + * the Instituto Federal de Educação Ciência e Tecnologia do Tocantins (IFTO, Brazil). + * + * This file is part of CloudSim Plus. + * + * CloudSim Plus is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * CloudSim Plus is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CloudSim Plus. If not, see . + */ +package org.cloudsimplus.benchmarks; + +import javax.management.JMException; +import javax.management.ObjectName; +import java.lang.management.ManagementFactory; +import java.lang.ref.Reference; +import java.util.Arrays; +import java.util.function.IntFunction; + +/// Measures the heap retained by objects (their memory footprint). +/// +/// It creates a number of objects, keeps them in an array and measures the bytes of all live objects +/// in the heap, before and after creating them. The difference divided by the number of objects +/// is the heap retained by each object (with everything it references). +/// Each measurement is repeated a number of rounds and the median is returned. +/// +/// The live bytes are got from a class histogram (the same as `jcmd GC.class_histogram`), +/// which runs a full GC and counts only reachable objects. The used heap from +/// [java.lang.management.MemoryMXBean] isn't used, since it may include objects not collected yet, +/// even after calling [System#gc()]. This way, it works with any GC. +/// +/// @author Manoel Campos da Silva Filho +final class HeapMeter { + private static final String DIAGNOSTIC_COMMAND_MBEAN = "com.sun.management:type=DiagnosticCommand"; + private static final String CLASS_HISTOGRAM_OPERATION = "gcClassHistogram"; + + /// A private constructor to avoid class instantiation. + private HeapMeter(){} + + /// {@return the median heap (in bytes) retained by each object created by a given function} + /// @param factory a function that creates an object for a given index + /// @param objects number of objects to create in each round + /// @param rounds number of times the measurement is repeated + static double bytesPerObject(final IntFunction factory, final int objects, final int rounds) { + final double[] bytesPerObject = new double[rounds]; + for (int round = 0; round < rounds; round++) { + bytesPerObject[round] = bytesPerObject(factory, objects); + } + + return median(bytesPerObject); + } + + /// {@return the heap (in bytes) retained by each object created by a given function in a single round} + /// The array that keeps the objects alive is created before the first reading, + /// so it isn't included in the measured size. + private static double bytesPerObject(final IntFunction factory, final int objects) { + final Object[] array = new Object[objects]; + final long liveBefore = liveBytes(); + for (int i = 0; i < objects; i++) { + array[i] = factory.apply(i); + } + + final long liveAfter = liveBytes(); + Reference.reachabilityFence(array); + return (liveAfter - liveBefore) / (double) objects; + } + + /// {@return the bytes of all live objects in the heap, after a full GC} + /// It reads the total from the last line of the class histogram, + /// which has the format `Total `. + static long liveBytes() { + final String[] lines = classHistogram().strip().split("\n"); + final String[] total = lines[lines.length - 1].trim().split("\\s+"); + return Long.parseLong(total[2]); + } + + private static String classHistogram() { + try { + final Object[] params = {null}; + final String[] signature = {String[].class.getName()}; + return (String) ManagementFactory.getPlatformMBeanServer() + .invoke(new ObjectName(DIAGNOSTIC_COMMAND_MBEAN), CLASS_HISTOGRAM_OPERATION, params, signature); + } catch (final JMException e) { + throw new IllegalStateException("Couldn't get the class histogram", e); + } + } + + /// {@return the median of some values} + static double median(final double[] values) { + final double[] sorted = values.clone(); + Arrays.sort(sorted); + final int middle = sorted.length / 2; + return sorted.length % 2 == 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle]; + } +} diff --git a/src/main/java/org/cloudsimplus/benchmarks/ProcessorBenchmark.java b/src/main/java/org/cloudsimplus/benchmarks/ProcessorBenchmark.java new file mode 100644 index 0000000..a53fada --- /dev/null +++ b/src/main/java/org/cloudsimplus/benchmarks/ProcessorBenchmark.java @@ -0,0 +1,151 @@ +/* + * CloudSim Plus: A modern, highly-extensible and easier-to-use Framework for + * Modeling and Simulation of Cloud Computing Infrastructures and Services. + * http://cloudsimplus.org + * + * Copyright (C) 2015-2021 Universidade da Beira Interior (UBI, Portugal) and + * the Instituto Federal de Educação Ciência e Tecnologia do Tocantins (IFTO, Brazil). + * + * This file is part of CloudSim Plus. + * + * CloudSim Plus is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * CloudSim Plus is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CloudSim Plus. If not, see . + */ +package org.cloudsimplus.benchmarks; + +import org.cloudsimplus.util.Log; + +import java.util.function.IntFunction; + +import static org.cloudsimplus.benchmarks.ProcessorScenario.*; + +/// Measures the memory footprint and simulation time reduction from +/// [cloudsimplus#595](https://github.com/cloudsimplus/cloudsimplus/pull/595) +/// ([#592](https://github.com/cloudsimplus/cloudsimplus/issues/592)), which replaced +/// the `List` of Hosts and VMs (one object per CPU core) by a single +/// `Processor` that just counts the cores in each status. +/// +/// It measures: +/// 1. the heap retained by each Host and VM, for different numbers of cores. +/// With a `List`, it grows with the number of cores. With a single `Processor`, it doesn't. +/// 2. the heap retained by a whole [ProcessorScenario] (Hosts, VMs, Cloudlets, broker and Datacenter), +/// before and after running it. +/// 3. the time to run that scenario. +/// +/// Each value is the median of [#ROUNDS] rounds (see [HeapMeter]). The simulation time is measured +/// after [#WARMUP_ROUNDS] runs, so that the JIT compiler has already optimized the code. +/// It's a plain `main` (not JMH), since JMH measures time and allocation rate, +/// but here we also need the memory that stays in the heap. +/// +/// This version uses the CloudSim Plus API before #595, where each Host has a `List`. +/// Run it with CloudSim Plus built from the commit right before #595: +/// +/// ```bash +/// git -C ../cloudsimplus checkout 9bb9de25c +/// (cd ../cloudsimplus && ./mvnw install -DskipTests) +/// ./mvnw clean package +/// java -Xmx4g -cp target/cloudsimplus-benchmarks-*-uber.jar org.cloudsimplus.benchmarks.ProcessorBenchmark +/// ``` +/// +/// @author Manoel Campos da Silva Filho +public final class ProcessorBenchmark { + /// Number of times each measurement is repeated. + private static final int ROUNDS = 5; + + /// Number of simulation runs before measuring the simulation time. + private static final int WARMUP_ROUNDS = 2; + + /// Number of objects created in each round to measure the heap retained by each Host or VM. + /// It's smaller than in [MemoryFootprintBenchmark], since Hosts with many cores + /// take a lot of memory when each core is an object. + private static final int OBJECTS = 10_000; + + /// Numbers of cores of the Hosts and VMs measured. + private static final int[] CORES = {1, 8, 32, 128}; + + private static final double MIPS = 1000; + private static final double BYTES_PER_MEGABYTE = 1024 * 1024; + private static final long HOST_RAM = 4096; + private static final long HOST_BW = 10_000; + private static final long HOST_STORAGE = 100_000; + + /// A description of the CloudSim Plus API used to create Hosts and VMs. + private static final String API = "List (before cloudsimplus#595)"; + + /// A private constructor to avoid class instantiation. + private ProcessorBenchmark(){} + + public static void main(final String[] args) { + Log.setLevel(ch.qos.logback.classic.Level.OFF); + System.out.printf("%s - CloudSim Plus API: %s%n%n", ProcessorBenchmark.class.getSimpleName(), API); + + printMachinesFootprint(); + printScenarioFootprint(); + printSimulationTime(); + } + + private static void printMachinesFootprint() { + System.out.printf("Heap retained by each machine (bytes), median of %d rounds with %,d objects:%n", ROUNDS, OBJECTS); + System.out.printf(" %5s | %12s | %12s%n", "Cores", "HostSimple", "VmSimple"); + for (final int cores : CORES) { + final double host = bytesPerObject(i -> newHost(HOST_RAM, HOST_BW, HOST_STORAGE, cores, MIPS)); + final double vm = bytesPerObject(i -> newVm(cores, MIPS)); + System.out.printf(" %5d | %,12.1f | %,12.1f%n", cores, host, vm); + } + + System.out.println(); + } + + private static double bytesPerObject(final IntFunction factory) { + return HeapMeter.bytesPerObject(factory, OBJECTS, ROUNDS); + } + + private static void printScenarioFootprint() { + System.out.printf("Scenario: %,d Hosts with %d cores, %,d VMs with %d cores, %,d Cloudlets with %d cores%n", + HOSTS, HOST_CORES, HOSTS * VMS_PER_HOST, VM_CORES, + HOSTS * VMS_PER_HOST * CLOUDLETS_PER_VM, CLOUDLET_CORES); + + final double before = HeapMeter.bytesPerObject(i -> new ProcessorScenario(), 1, ROUNDS); + System.out.printf(" Heap retained by the scenario before running it: %,.1f MB%n", toMegabytes(before)); + + final double after = HeapMeter.bytesPerObject(i -> newFinishedScenario(), 1, ROUNDS); + System.out.printf(" Heap retained by the scenario after running it: %,.1f MB%n", toMegabytes(after)); + } + + /// {@return a scenario that has already run} + /// Since it's kept, its measured size includes the objects created while the simulation runs + /// (such as Cloudlet executions and VM allocations). + private static ProcessorScenario newFinishedScenario() { + final var scenario = new ProcessorScenario(); + scenario.run(); + return scenario; + } + + private static double toMegabytes(final double bytes) { + return bytes / BYTES_PER_MEGABYTE; + } + + private static void printSimulationTime() { + for (int i = 0; i < WARMUP_ROUNDS; i++) { + new ProcessorScenario().run(); + } + + final double[] seconds = new double[ROUNDS]; + for (int i = 0; i < ROUNDS; i++) { + seconds[i] = new ProcessorScenario().run(); + } + + System.out.printf(" Simulation time: %.3f seconds (median of %d runs after %d warmup runs)%n", + HeapMeter.median(seconds), ROUNDS, WARMUP_ROUNDS); + } +} diff --git a/src/main/java/org/cloudsimplus/benchmarks/ProcessorScenario.java b/src/main/java/org/cloudsimplus/benchmarks/ProcessorScenario.java new file mode 100644 index 0000000..8b4b473 --- /dev/null +++ b/src/main/java/org/cloudsimplus/benchmarks/ProcessorScenario.java @@ -0,0 +1,151 @@ +/* + * CloudSim Plus: A modern, highly-extensible and easier-to-use Framework for + * Modeling and Simulation of Cloud Computing Infrastructures and Services. + * http://cloudsimplus.org + * + * Copyright (C) 2015-2021 Universidade da Beira Interior (UBI, Portugal) and + * the Instituto Federal de Educação Ciência e Tecnologia do Tocantins (IFTO, Brazil). + * + * This file is part of CloudSim Plus. + * + * CloudSim Plus is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * CloudSim Plus is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with CloudSim Plus. If not, see . + */ +package org.cloudsimplus.benchmarks; + +import org.cloudsimplus.allocationpolicies.VmAllocationPolicyBestFit; +import org.cloudsimplus.brokers.DatacenterBroker; +import org.cloudsimplus.brokers.DatacenterBrokerSimple; +import org.cloudsimplus.cloudlets.Cloudlet; +import org.cloudsimplus.cloudlets.CloudletSimple; +import org.cloudsimplus.core.CloudSimPlus; +import org.cloudsimplus.datacenters.DatacenterSimple; +import org.cloudsimplus.hosts.Host; +import org.cloudsimplus.hosts.HostSimple; +import org.cloudsimplus.resources.Pe; +import org.cloudsimplus.resources.PeSimple; +import org.cloudsimplus.schedulers.vm.VmSchedulerTimeShared; +import org.cloudsimplus.vms.Vm; +import org.cloudsimplus.vms.VmSimple; + +import java.util.ArrayList; +import java.util.List; + +/// A simulation scenario used by [ProcessorBenchmark], where Hosts and VMs have many CPU cores. +/// It's built when the object is created and executed by [#run()]. +/// +/// Hosts and VMs are created only by [#newHost(long, long, long, long, double)] and [#newVm(long, double)], +/// which use the CloudSim Plus API where a Host has a `List`, with one [Pe] object per CPU core. +/// +/// Cloudlets have different lengths, so they finish at different times. +/// This way, the simulation processes many events that change the number of +/// busy cores of VMs and Hosts, instead of finishing all Cloudlets at once. +/// +/// @author Manoel Campos da Silva Filho +final class ProcessorScenario { + static final int HOSTS = 1000; + static final int HOST_CORES = 32; + static final int VMS_PER_HOST = 4; + static final int VM_CORES = HOST_CORES / VMS_PER_HOST; + static final int CLOUDLETS_PER_VM = 2; + static final int CLOUDLET_CORES = VM_CORES / CLOUDLETS_PER_VM; + + private static final double MIPS = 1000; + private static final long HOST_RAM = 65_536; + private static final long HOST_BW = 100_000; + private static final long HOST_STORAGE = 1_000_000; + private static final long VM_RAM = HOST_RAM / VMS_PER_HOST; + private static final long VM_BW = HOST_BW / VMS_PER_HOST; + private static final long VM_SIZE = HOST_STORAGE / VMS_PER_HOST; + + /// The length of the shortest Cloudlet (in MI). + private static final long CLOUDLET_MIN_LENGTH = 10_000; + + /// Number of different Cloudlet lengths, which are multiples of [#CLOUDLET_MIN_LENGTH]. + private static final int CLOUDLET_LENGTHS = 20; + + private final CloudSimPlus simulation; + + ProcessorScenario() { + simulation = new CloudSimPlus(); + final var datacenter = new DatacenterSimple(simulation, createHosts(), new VmAllocationPolicyBestFit()); + datacenter.setSchedulingInterval(1); + + final DatacenterBroker broker = new DatacenterBrokerSimple(simulation); + final List vms = createVms(); + broker.submitVmList(vms); + broker.submitCloudletList(createCloudlets()); + } + + /// Runs the simulation. + /// @return the time (in seconds) to run the simulation + double run() { + final long start = System.nanoTime(); + simulation.start(); + return (System.nanoTime() - start) / 1e9; + } + + private static List createHosts() { + final var hosts = new ArrayList(HOSTS); + for (int i = 0; i < HOSTS; i++) { + final Host host = newHost(HOST_RAM, HOST_BW, HOST_STORAGE, HOST_CORES, MIPS); + host.setVmScheduler(new VmSchedulerTimeShared()); + hosts.add(host); + } + + return hosts; + } + + private static List createVms() { + final int count = HOSTS * VMS_PER_HOST; + final var vms = new ArrayList(count); + for (int i = 0; i < count; i++) { + vms.add(newVm(VM_CORES, MIPS).setRam(VM_RAM).setBw(VM_BW).setSize(VM_SIZE)); + } + + return vms; + } + + private static List createCloudlets() { + final int count = HOSTS * VMS_PER_HOST * CLOUDLETS_PER_VM; + final var cloudlets = new ArrayList(count); + for (int i = 0; i < count; i++) { + final long length = CLOUDLET_MIN_LENGTH * (1 + i % CLOUDLET_LENGTHS); + cloudlets.add(new CloudletSimple(length, CLOUDLET_CORES)); + } + + return cloudlets; + } + + /// {@return a new Host} + /// @param ram the RAM capacity (in Megabytes) + /// @param bw the bandwidth capacity (in Megabits/s) + /// @param storage the storage capacity (in Megabytes) + /// @param cores the number of CPU cores + /// @param mips the MIPS capacity of each core + static HostSimple newHost(final long ram, final long bw, final long storage, final long cores, final double mips) { + final List peList = new ArrayList<>((int) cores); + for (int i = 0; i < cores; i++) { + peList.add(new PeSimple(mips)); + } + + return new HostSimple(ram, bw, storage, peList); + } + + /// {@return a new VM} + /// @param cores the number of CPU cores + /// @param mips the MIPS capacity of each core + static VmSimple newVm(final long cores, final double mips) { + return new VmSimple(mips, cores); + } +}