Browse Source

Reduce size of idle and actively used ProfilingPlugin (#103355)

We can dedup about 1.5M of strings and value objects here.
Also, there's no need to be so tricky in loading the data in the first place,
just lazy loading into a compact immutable map via the holder pattern achieves the same thing
and is free heap-wise so long as the plugin isn't actively used.
Armin Braun 1 year ago
parent
commit
fb5c74475b

+ 1 - 4
x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CO2Calculator.java

@@ -18,7 +18,6 @@ final class CO2Calculator {
     private static final double DEFAULT_KILOWATTS_PER_CORE_ARM64 = 2.8d / 1000.0d; // unit: watt / core
     private static final double DEFAULT_KILOWATTS_PER_CORE = DEFAULT_KILOWATTS_PER_CORE_X86; // unit: watt / core
     private static final double DEFAULT_DATACENTER_PUE = 1.7d;
-    private final InstanceTypeService instanceTypeService;
     private final Map<String, HostMetadata> hostMetadata;
     private final double samplingDurationInSeconds;
     private final double customCO2PerKWH;
@@ -27,7 +26,6 @@ final class CO2Calculator {
     private final double customKilowattsPerCoreARM64;
 
     CO2Calculator(
-        InstanceTypeService instanceTypeService,
         Map<String, HostMetadata> hostMetadata,
         double samplingDurationInSeconds,
         Double customCO2PerKWH,
@@ -35,7 +33,6 @@ final class CO2Calculator {
         Double customPerCoreWattX86,
         Double customPerCoreWattARM64
     ) {
-        this.instanceTypeService = instanceTypeService;
         this.hostMetadata = hostMetadata;
         this.samplingDurationInSeconds = samplingDurationInSeconds > 0 ? samplingDurationInSeconds : 1.0d; // avoid division by zero
         this.customCO2PerKWH = customCO2PerKWH == null ? DEFAULT_CO2_TONS_PER_KWH : customCO2PerKWH;
@@ -54,7 +51,7 @@ final class CO2Calculator {
             return DEFAULT_KILOWATTS_PER_CORE * customCO2PerKWH * annualCoreHours * customDatacenterPUE;
         }
 
-        CostEntry costs = instanceTypeService.getCosts(host.instanceType);
+        CostEntry costs = InstanceTypeService.getCosts(host.instanceType);
         if (costs == null) {
             return getKiloWattsPerCore(host) * getCO2TonsPerKWH(host) * annualCoreHours * getDatacenterPUE(host);
         }

+ 1 - 4
x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/CostCalculator.java

@@ -15,20 +15,17 @@ final class CostCalculator {
     private static final double SECONDS_PER_YEAR = SECONDS_PER_HOUR * 24 * 365.0d; // unit: seconds
     private static final double DEFAULT_COST_USD_PER_CORE_HOUR = 0.0425d; // unit: USD / (core * hour)
     private static final double DEFAULT_AWS_COST_FACTOR = 1.0d;
-    private final InstanceTypeService instanceTypeService;
     private final Map<String, HostMetadata> hostMetadata;
     private final double samplingDurationInSeconds;
     private final double awsCostFactor;
     private final double customCostPerCoreHour;
 
     CostCalculator(
-        InstanceTypeService instanceTypeService,
         Map<String, HostMetadata> hostMetadata,
         double samplingDurationInSeconds,
         Double awsCostFactor,
         Double customCostPerCoreHour
     ) {
-        this.instanceTypeService = instanceTypeService;
         this.hostMetadata = hostMetadata;
         this.samplingDurationInSeconds = samplingDurationInSeconds > 0 ? samplingDurationInSeconds : 1.0d; // avoid division by zero
         this.awsCostFactor = awsCostFactor == null ? DEFAULT_AWS_COST_FACTOR : awsCostFactor;
@@ -45,7 +42,7 @@ final class CostCalculator {
 
         double providerCostFactor = host.instanceType.provider.equals("aws") ? awsCostFactor : 1.0d;
 
-        CostEntry costs = instanceTypeService.getCosts(host.instanceType);
+        CostEntry costs = InstanceTypeService.getCosts(host.instanceType);
         if (costs == null) {
             return annualCoreHours * customCostPerCoreHour * providerCostFactor;
         }

+ 0 - 10
x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/InstanceType.java

@@ -28,16 +28,6 @@ final class InstanceType implements ToXContentObject {
         this.name = name;
     }
 
-    /**
-     * Creates a {@link InstanceType} from a {@link Map} of source data provided from JSON or profiling-costs.
-     *
-     * @param source the source data
-     * @return the {@link InstanceType}
-     */
-    public static InstanceType fromCostSource(Map<String, Object> source) {
-        return new InstanceType((String) source.get("provider"), (String) source.get("region"), (String) source.get("instance_type"));
-    }
-
     /**
      * Creates a {@link InstanceType} from a {@link Map} of source data provided from profiling-hosts.
      *

+ 37 - 22
x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/InstanceTypeService.java

@@ -13,36 +13,51 @@ import org.elasticsearch.xcontent.XContentParserConfiguration;
 import org.elasticsearch.xcontent.XContentType;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import java.util.zip.GZIPInputStream;
 
-public class InstanceTypeService {
-    private final Map<InstanceType, CostEntry> costsPerDatacenter = new HashMap<>();
-
-    public void load() {
-        try (
-            GZIPInputStream in = new GZIPInputStream(
-                InstanceTypeService.class.getClassLoader().getResourceAsStream("profiling-costs.json.gz")
-            )
-        ) {
-            XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, in);
-            if (parser.currentToken() == null) {
-                parser.nextToken();
-            }
-            List<Map<String, Object>> rawData = XContentParserUtils.parseList(parser, XContentParser::map);
-            for (Map<String, Object> entry : rawData) {
-                costsPerDatacenter.put(InstanceType.fromCostSource(entry), CostEntry.fromSource(entry));
-            }
+public final class InstanceTypeService {
+
+    private InstanceTypeService() {}
 
-        } catch (IOException e) {
-            throw new UncheckedIOException(e);
+    private static final class Holder {
+        private static final Map<InstanceType, CostEntry> costsPerDatacenter;
+
+        static {
+            final Map<Object, Object> objects = new HashMap<>();
+            final Function<String, String> dedupString = s -> (String) objects.computeIfAbsent(s, Function.identity());
+            final Map<InstanceType, CostEntry> tmp = new HashMap<>();
+            try (
+                GZIPInputStream in = new GZIPInputStream(
+                    InstanceTypeService.class.getClassLoader().getResourceAsStream("profiling-costs.json.gz")
+                );
+                XContentParser parser = XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, in)
+            ) {
+                if (parser.currentToken() == null) {
+                    parser.nextToken();
+                }
+                List<Map<String, Object>> rawData = XContentParserUtils.parseList(parser, XContentParser::map);
+                for (Map<String, Object> entry : rawData) {
+                    tmp.put(
+                        new InstanceType(
+                            dedupString.apply((String) entry.get("provider")),
+                            dedupString.apply((String) entry.get("region")),
+                            dedupString.apply((String) entry.get("instance_type"))
+                        ),
+                        (CostEntry) objects.computeIfAbsent(CostEntry.fromSource(entry), Function.identity())
+                    );
+                }
+                costsPerDatacenter = Map.copyOf(tmp);
+            } catch (IOException e) {
+                throw new ExceptionInInitializerError(e);
+            }
         }
     }
 
-    public CostEntry getCosts(InstanceType instance) {
-        return costsPerDatacenter.get(instance);
+    public static CostEntry getCosts(InstanceType instance) {
+        return Holder.costsPerDatacenter.get(instance);
     }
 }

+ 1 - 7
x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/ProfilingPlugin.java

@@ -86,24 +86,18 @@ public class ProfilingPlugin extends Plugin implements ActionPlugin {
         // set initial value
         updateTemplatesEnabled(PROFILING_TEMPLATES_ENABLED.get(settings));
         clusterService.getClusterSettings().addSettingsUpdateConsumer(PROFILING_TEMPLATES_ENABLED, this::updateTemplatesEnabled);
-        InstanceTypeService instanceTypeService = createInstanceTypeService();
         if (enabled) {
             registry.get().initialize();
             indexManager.get().initialize();
             dataStreamManager.get().initialize();
-            instanceTypeService.load();
         }
-        return List.of(createLicenseChecker(), instanceTypeService);
+        return List.of(createLicenseChecker());
     }
 
     protected ProfilingLicenseChecker createLicenseChecker() {
         return new ProfilingLicenseChecker(XPackPlugin::getSharedLicenseState);
     }
 
-    protected InstanceTypeService createInstanceTypeService() {
-        return new InstanceTypeService();
-    }
-
     public void updateCheckOutdatedIndices(boolean newValue) {
         if (newValue == false) {
             logger.info("profiling will ignore outdated indices");

+ 0 - 5
x-pack/plugin/profiling/src/main/java/org/elasticsearch/xpack/profiling/TransportGetStackTracesAction.java

@@ -110,7 +110,6 @@ public class TransportGetStackTracesAction extends TransportAction<GetStackTrace
 
     private final NodeClient nodeClient;
     private final ProfilingLicenseChecker licenseChecker;
-    private final InstanceTypeService instanceTypeService;
     private final ClusterService clusterService;
     private final TransportService transportService;
     private final Executor responseExecutor;
@@ -129,13 +128,11 @@ public class TransportGetStackTracesAction extends TransportAction<GetStackTrace
         ActionFilters actionFilters,
         NodeClient nodeClient,
         ProfilingLicenseChecker licenseChecker,
-        InstanceTypeService instanceTypeService,
         IndexNameExpressionResolver resolver
     ) {
         super(GetStackTracesAction.NAME, actionFilters, transportService.getTaskManager());
         this.nodeClient = nodeClient;
         this.licenseChecker = licenseChecker;
-        this.instanceTypeService = instanceTypeService;
         this.clusterService = clusterService;
         this.transportService = transportService;
         this.responseExecutor = threadPool.executor(ProfilingPlugin.PROFILING_THREAD_POOL_NAME);
@@ -549,7 +546,6 @@ public class TransportGetStackTracesAction extends TransportAction<GetStackTrace
             // Do the CO2 and cost calculation in parallel to waiting for frame metadata.
             StopWatch watch = new StopWatch("calculateCO2AndCosts");
             CO2Calculator co2Calculator = new CO2Calculator(
-                instanceTypeService,
                 hostMetadata,
                 responseBuilder.getRequestedDuration(),
                 responseBuilder.getCustomCO2PerKWH(),
@@ -558,7 +554,6 @@ public class TransportGetStackTracesAction extends TransportAction<GetStackTrace
                 responseBuilder.getCustomPerCoreWattARM64()
             );
             CostCalculator costCalculator = new CostCalculator(
-                instanceTypeService,
                 hostMetadata,
                 responseBuilder.getRequestedDuration(),
                 responseBuilder.getAWSCostFactor(),

+ 1 - 4
x-pack/plugin/profiling/src/test/java/org/elasticsearch/xpack/profiling/CO2CalculatorTests.java

@@ -18,9 +18,6 @@ public class CO2CalculatorTests extends ESTestCase {
     private static final String HOST_ID_D = "4440256254710195394";
 
     public void testCreateFromRegularSource() {
-        InstanceTypeService instanceTypeService = new InstanceTypeService();
-        instanceTypeService.load();
-
         // tag::noformat
         Map<String, HostMetadata> hostsTable = Map.ofEntries(
             Map.entry(HOST_ID_A,
@@ -73,7 +70,7 @@ public class CO2CalculatorTests extends ESTestCase {
         double samplingDurationInSeconds = 1_800.0d; // 30 minutes
         long samples = 100_000L; // 100k samples
         double annualCoreHours = CostCalculator.annualCoreHours(samplingDurationInSeconds, samples, 20.0d);
-        CO2Calculator co2Calculator = new CO2Calculator(instanceTypeService, hostsTable, samplingDurationInSeconds, null, null, null, null);
+        CO2Calculator co2Calculator = new CO2Calculator(hostsTable, samplingDurationInSeconds, null, null, null, null);
 
         checkCO2Calculation(co2Calculator.getAnnualCO2Tons(HOST_ID_A, samples), annualCoreHours, 0.000002213477d);
         checkCO2Calculation(co2Calculator.getAnnualCO2Tons(HOST_ID_B, samples), annualCoreHours, 1.1d, 0.00004452d, 7.0d);

+ 1 - 4
x-pack/plugin/profiling/src/test/java/org/elasticsearch/xpack/profiling/CostCalculatorTests.java

@@ -16,9 +16,6 @@ public class CostCalculatorTests extends ESTestCase {
     private static final String HOST_ID_B = "2220256254710195392";
 
     public void testCreateFromRegularSource() {
-        InstanceTypeService instanceTypeService = new InstanceTypeService();
-        instanceTypeService.load();
-
         // tag::noformat
         Map<String, HostMetadata> hostsTable = Map.ofEntries(
             Map.entry(HOST_ID_A,
@@ -49,7 +46,7 @@ public class CostCalculatorTests extends ESTestCase {
         double samplingDurationInSeconds = 1_800.0d; // 30 minutes
         long samples = 100_000L; // 100k samples
         double annualCoreHours = CostCalculator.annualCoreHours(samplingDurationInSeconds, samples, 20.0d);
-        CostCalculator costCalculator = new CostCalculator(instanceTypeService, hostsTable, samplingDurationInSeconds, null, null);
+        CostCalculator costCalculator = new CostCalculator(hostsTable, samplingDurationInSeconds, null, null);
 
         // Checks whether the cost calculation is based on the pre-calculated lookup data.
         checkCostCalculation(costCalculator.annualCostsUSD(HOST_ID_A, samples), annualCoreHours, 0.061d);