aboutsummaryrefslogtreecommitdiff
path: root/spark-common/src/main/java/me
diff options
context:
space:
mode:
authorLuck <git@lucko.me>2022-04-18 11:54:40 +0100
committerLuck <git@lucko.me>2022-04-18 11:54:40 +0100
commit45edd6197aa777d0c746f37a813d6be4ca916694 (patch)
tree967c3998eb21a363e9896654dccd86e70e0368b0 /spark-common/src/main/java/me
parentecc9ac3fdb731ef3beee2ce45ddf4b355b12745f (diff)
downloadspark-45edd6197aa777d0c746f37a813d6be4ca916694.tar.gz
spark-45edd6197aa777d0c746f37a813d6be4ca916694.tar.bz2
spark-45edd6197aa777d0c746f37a813d6be4ca916694.zip
Include network interface statistics in '/spark health' command
Diffstat (limited to 'spark-common/src/main/java/me')
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java2
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java51
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/monitor/net/Direction.java37
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceAverages.java88
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceInfo.java274
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkMonitor.java140
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/monitor/ping/PingStatistics.java5
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java30
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/platform/serverconfig/AbstractServerConfigProvider.java2
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java23
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java7
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java11
12 files changed, 651 insertions, 19 deletions
diff --git a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java
index a087fc9..a961925 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java
@@ -40,6 +40,7 @@ import me.lucko.spark.common.command.tabcomplete.CompletionSupplier;
import me.lucko.spark.common.command.tabcomplete.TabCompleter;
import me.lucko.spark.common.monitor.cpu.CpuMonitor;
import me.lucko.spark.common.monitor.memory.GarbageCollectorStatistics;
+import me.lucko.spark.common.monitor.net.NetworkMonitor;
import me.lucko.spark.common.monitor.ping.PingStatistics;
import me.lucko.spark.common.monitor.ping.PlayerPingProvider;
import me.lucko.spark.common.monitor.tick.TickStatistics;
@@ -166,6 +167,7 @@ public class SparkPlatform {
this.pingStatistics.start();
}
CpuMonitor.ensureMonitoring();
+ NetworkMonitor.ensureMonitoring();
// poll startup GC statistics after plugins & the world have loaded
this.plugin.executeAsync(() -> {
diff --git a/spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java b/spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java
index ea4f140..ee3592a 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/command/modules/HealthModule.java
@@ -29,6 +29,9 @@ import me.lucko.spark.common.command.sender.CommandSender;
import me.lucko.spark.common.command.tabcomplete.TabCompleter;
import me.lucko.spark.common.monitor.cpu.CpuMonitor;
import me.lucko.spark.common.monitor.disk.DiskUsage;
+import me.lucko.spark.common.monitor.net.Direction;
+import me.lucko.spark.common.monitor.net.NetworkInterfaceAverages;
+import me.lucko.spark.common.monitor.net.NetworkMonitor;
import me.lucko.spark.common.monitor.ping.PingStatistics;
import me.lucko.spark.common.monitor.ping.PingSummary;
import me.lucko.spark.common.monitor.tick.TickStatistics;
@@ -45,6 +48,7 @@ import java.lang.management.MemoryType;
import java.lang.management.MemoryUsage;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
@@ -81,8 +85,9 @@ public class HealthModule implements CommandModule {
consumer.accept(Command.builder()
.aliases("healthreport", "health", "ht")
.argumentUsage("memory", null)
+ .argumentUsage("network", null)
.executor(HealthModule::healthReport)
- .tabCompleter((platform, sender, arguments) -> TabCompleter.completeForOpts(arguments, "--memory"))
+ .tabCompleter((platform, sender, arguments) -> TabCompleter.completeForOpts(arguments, "--memory", "--network"))
.build()
);
}
@@ -197,6 +202,8 @@ public class HealthModule implements CommandModule {
addDetailedMemoryStats(report, memoryMXBean);
}
+ addNetworkStats(report, arguments.boolFlag("network"));
+
addDiskStats(report);
resp.reply(report);
@@ -352,6 +359,48 @@ public class HealthModule implements CommandModule {
}
}
+ private static void addNetworkStats(List<Component> report, boolean detailed) {
+ List<Component> averagesReport = new LinkedList<>();
+
+ for (Map.Entry<String, NetworkInterfaceAverages> ent : NetworkMonitor.systemAverages().entrySet()) {
+ String interfaceName = ent.getKey();
+ NetworkInterfaceAverages averages = ent.getValue();
+
+ for (Direction direction : Direction.values()) {
+ long bytesPerSec = (long) averages.bytesPerSecond(direction).mean();
+ long packetsPerSec = (long) averages.packetsPerSecond(direction).mean();
+
+ if (detailed || bytesPerSec > 0 || packetsPerSec > 0) {
+ averagesReport.add(text()
+ .color(GRAY)
+ .content(" ")
+ .append(FormatUtil.formatBytes(bytesPerSec, GREEN, "/s"))
+ .append(text(" / "))
+ .append(text(String.format("%,d", packetsPerSec), WHITE))
+ .append(text(" pps "))
+ .append(text().color(DARK_GRAY)
+ .append(text('('))
+ .append(text(interfaceName + " " + direction.abbrev(), WHITE))
+ .append(text(')'))
+ )
+ .build()
+ );
+ }
+ }
+ }
+
+ if (!averagesReport.isEmpty()) {
+ report.add(text()
+ .append(text(">", DARK_GRAY, BOLD))
+ .append(space())
+ .append(text("Network usage: (system, last 15m)", GOLD))
+ .build()
+ );
+ report.addAll(averagesReport);
+ report.add(empty());
+ }
+ }
+
private static void addDiskStats(List<Component> report) {
long total = DiskUsage.getTotal();
long used = DiskUsage.getUsed();
diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/net/Direction.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/Direction.java
new file mode 100644
index 0000000..d4d11ff
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/Direction.java
@@ -0,0 +1,37 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.common.monitor.net;
+
+public enum Direction {
+
+ RECEIVE("rx"),
+ TRANSMIT("tx");
+
+ private final String abbreviation;
+
+ Direction(String abbreviation) {
+ this.abbreviation = abbreviation;
+ }
+
+ public String abbrev() {
+ return this.abbreviation;
+ }
+}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceAverages.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceAverages.java
new file mode 100644
index 0000000..0ce0639
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceAverages.java
@@ -0,0 +1,88 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.common.monitor.net;
+
+import me.lucko.spark.common.util.RollingAverage;
+
+import java.math.BigDecimal;
+
+public final class NetworkInterfaceAverages {
+ private final RollingAverage rxBytesPerSecond;
+ private final RollingAverage txBytesPerSecond;
+ private final RollingAverage rxPacketsPerSecond;
+ private final RollingAverage txPacketsPerSecond;
+
+ NetworkInterfaceAverages(int windowSize) {
+ this.rxBytesPerSecond = new RollingAverage(windowSize);
+ this.txBytesPerSecond = new RollingAverage(windowSize);
+ this.rxPacketsPerSecond = new RollingAverage(windowSize);
+ this.txPacketsPerSecond = new RollingAverage(windowSize);
+ }
+
+ void accept(NetworkInterfaceInfo info, RateCalculator rateCalculator) {
+ this.rxBytesPerSecond.add(rateCalculator.calculate(info.getReceivedBytes()));
+ this.txBytesPerSecond.add(rateCalculator.calculate(info.getTransmittedBytes()));
+ this.rxPacketsPerSecond.add(rateCalculator.calculate(info.getReceivedPackets()));
+ this.txPacketsPerSecond.add(rateCalculator.calculate(info.getTransmittedPackets()));
+ }
+
+ interface RateCalculator {
+ BigDecimal calculate(long value);
+ }
+
+ public RollingAverage bytesPerSecond(Direction direction) {
+ switch (direction) {
+ case RECEIVE:
+ return rxBytesPerSecond();
+ case TRANSMIT:
+ return txBytesPerSecond();
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ public RollingAverage packetsPerSecond(Direction direction) {
+ switch (direction) {
+ case RECEIVE:
+ return rxPacketsPerSecond();
+ case TRANSMIT:
+ return txPacketsPerSecond();
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ public RollingAverage rxBytesPerSecond() {
+ return this.rxBytesPerSecond;
+ }
+
+ public RollingAverage rxPacketsPerSecond() {
+ return this.rxPacketsPerSecond;
+ }
+
+ public RollingAverage txBytesPerSecond() {
+ return this.txBytesPerSecond;
+ }
+
+ public RollingAverage txPacketsPerSecond() {
+ return this.txPacketsPerSecond;
+ }
+}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceInfo.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceInfo.java
new file mode 100644
index 0000000..6ec700b
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkInterfaceInfo.java
@@ -0,0 +1,274 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.common.monitor.net;
+
+import com.google.common.collect.ImmutableMap;
+
+import me.lucko.spark.common.util.LinuxProc;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.IntStream;
+
+/**
+ * Exposes information/statistics about a network interface.
+ */
+public final class NetworkInterfaceInfo {
+ public static final NetworkInterfaceInfo ZERO = new NetworkInterfaceInfo("", 0, 0, 0, 0, 0, 0);
+
+ private final String name;
+ private final long rxBytes;
+ private final long rxPackets;
+ private final long rxErrors;
+ private final long txBytes;
+ private final long txPackets;
+ private final long txErrors;
+
+ public NetworkInterfaceInfo(String name, long rxBytes, long rxPackets, long rxErrors, long txBytes, long txPackets, long txErrors) {
+ this.name = name;
+ this.rxBytes = rxBytes;
+ this.rxPackets = rxPackets;
+ this.rxErrors = rxErrors;
+ this.txBytes = txBytes;
+ this.txPackets = txPackets;
+ this.txErrors = txErrors;
+ }
+
+ /**
+ * Gets the name of the network interface.
+ *
+ * @return the interface name
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Gets the total number of bytes of data received by the interface.
+ *
+ * @return the total received bytes
+ */
+ public long getReceivedBytes() {
+ return this.rxBytes;
+ }
+
+ /**
+ * Gets the total number of packets of data received by the interface.
+ *
+ * @return the total received packets
+ */
+ public long getReceivedPackets() {
+ return this.rxPackets;
+ }
+
+ /**
+ * Gets the total number of receive errors detected by the device driver.
+ *
+ * @return the total receive errors
+ */
+ public long getReceiveErrors() {
+ return this.rxErrors;
+ }
+
+ /**
+ * Gets the total number of bytes of data transmitted by the interface.
+ *
+ * @return the total transmitted bytes
+ */
+ public long getTransmittedBytes() {
+ return this.txBytes;
+ }
+
+ /**
+ * Gets the total number of packets of data transmitted by the interface.
+ *
+ * @return the total transmitted packets
+ */
+ public long getTransmittedPackets() {
+ return this.txPackets;
+ }
+
+ /**
+ * Gets the total number of transmit errors detected by the device driver.
+ *
+ * @return the total transmit errors
+ */
+ public long getTransmitErrors() {
+ return this.txErrors;
+ }
+
+ public long getBytes(Direction direction) {
+ switch (direction) {
+ case RECEIVE:
+ return getReceivedBytes();
+ case TRANSMIT:
+ return getTransmittedBytes();
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ public long getPackets(Direction direction) {
+ switch (direction) {
+ case RECEIVE:
+ return getReceivedPackets();
+ case TRANSMIT:
+ return getTransmittedPackets();
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ public boolean isZero() {
+ return this.rxBytes == 0 && this.rxPackets == 0 && this.rxErrors == 0 &&
+ this.txBytes == 0 && this.txPackets == 0 && this.txErrors == 0;
+ }
+
+ public NetworkInterfaceInfo subtract(NetworkInterfaceInfo other) {
+ if (other == ZERO || other.isZero()) {
+ return this;
+ }
+
+ return new NetworkInterfaceInfo(
+ this.name,
+ this.rxBytes - other.rxBytes,
+ this.rxPackets - other.rxPackets,
+ this.rxErrors - other.rxErrors,
+ this.txBytes - other.txBytes,
+ this.txPackets - other.txPackets,
+ this.txErrors - other.txErrors
+ );
+ }
+
+ /**
+ * Calculate the difference between two readings in order to calculate the rate.
+ *
+ * @param current the polled values
+ * @param previous the previously polled values
+ * @return the difference
+ */
+ public static @NonNull Map<String, NetworkInterfaceInfo> difference(Map<String, NetworkInterfaceInfo> current, Map<String, NetworkInterfaceInfo> previous) {
+ if (previous == null || previous.isEmpty()) {
+ return current;
+ }
+
+ ImmutableMap.Builder<String, NetworkInterfaceInfo> builder = ImmutableMap.builder();
+ for (NetworkInterfaceInfo netInf : current.values()) {
+ String name = netInf.getName();
+ builder.put(name, netInf.subtract(previous.getOrDefault(name, ZERO)));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Queries the network interface statistics for the system.
+ *
+ * <p>Returns an empty {@link Map} if no statistics could be gathered.</p>
+ *
+ * @return the system net stats
+ */
+ public static @NonNull Map<String, NetworkInterfaceInfo> pollSystem() {
+ try {
+ List<String> output = LinuxProc.NET_DEV.read();
+ return read(output);
+ } catch (Exception e) {
+ return Collections.emptyMap();
+ }
+ }
+
+ private static final Pattern PROC_NET_DEV_PATTERN = Pattern.compile("^\\s+(\\w+):([\\d\\s]+)$");
+
+ private static @NonNull Map<String, NetworkInterfaceInfo> read(List<String> output) {
+ // Inter-| Receive | Transmit
+ // face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
+ // lo: 2776770 11307 0 0 0 0 0 0 2776770 11307 0 0 0 0 0 0
+ // eth0: 1215645 2751 0 0 0 0 0 0 1782404 4324 0 0 0 427 0 0
+ // ppp0: 1622270 5552 1 0 0 0 0 0 354130 5669 0 0 0 0 0 0
+ // tap0: 7714 81 0 0 0 0 0 0 7714 81 0 0 0 0 0 0
+
+ if (output.size() < 3) {
+ // no data
+ return Collections.emptyMap();
+ }
+
+ String header = output.get(1);
+ String[] categories = header.split("\\|");
+ if (categories.length != 3) {
+ // unknown format
+ return Collections.emptyMap();
+ }
+
+ List<String> rxFields = Arrays.asList(categories[1].trim().split("\\s+"));
+ List<String> txFields = Arrays.asList(categories[2].trim().split("\\s+"));
+
+ int rxFieldsLength = rxFields.size();
+ int txFieldsLength = txFields.size();
+
+ int fieldRxBytes = rxFields.indexOf("bytes");
+ int fieldRxPackets = rxFields.indexOf("packets");
+ int fieldRxErrors = rxFields.indexOf("errs");
+
+ int fieldTxBytes = rxFieldsLength + txFields.indexOf("bytes");
+ int fieldTxPackets = rxFieldsLength + txFields.indexOf("packets");
+ int fieldTxErrors = rxFieldsLength + txFields.indexOf("errs");
+
+ int expectedFields = rxFieldsLength + txFieldsLength;
+
+ if (IntStream.of(fieldRxBytes, fieldRxPackets, fieldRxErrors, fieldTxBytes, fieldTxPackets, fieldTxErrors).anyMatch(i -> i == -1)) {
+ // missing required fields
+ return Collections.emptyMap();
+ }
+
+ ImmutableMap.Builder<String, NetworkInterfaceInfo> builder = ImmutableMap.builder();
+
+ for (String line : output.subList(2, output.size())) {
+ Matcher matcher = PROC_NET_DEV_PATTERN.matcher(line);
+ if (matcher.matches()) {
+ String interfaceName = matcher.group(1);
+ String[] stringValues = matcher.group(2).trim().split("\\s+");
+
+ if (stringValues.length != expectedFields) {
+ continue;
+ }
+
+ long[] values = Arrays.stream(stringValues).mapToLong(Long::parseLong).toArray();
+ builder.put(interfaceName, new NetworkInterfaceInfo(
+ interfaceName,
+ values[fieldRxBytes],
+ values[fieldRxPackets],
+ values[fieldRxErrors],
+ values[fieldTxBytes],
+ values[fieldTxPackets],
+ values[fieldTxErrors]
+ ));
+ }
+ }
+
+ return builder.build();
+ }
+
+}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkMonitor.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkMonitor.java
new file mode 100644
index 0000000..dadd4e5
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/net/NetworkMonitor.java
@@ -0,0 +1,140 @@
+/*
+ * This file is part of spark.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package me.lucko.spark.common.monitor.net;
+
+import me.lucko.spark.common.monitor.MonitoringExecutor;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+/**
+ * Exposes and monitors the system/process network usage.
+ */
+public enum NetworkMonitor {
+ ;
+
+ // Latest readings
+ private static final AtomicReference<Map<String, NetworkInterfaceInfo>> SYSTEM = new AtomicReference<>();
+
+ // a pattern to match the interface names to calculate rolling averages for
+ private static final Pattern INTERFACES_TO_MONITOR = Pattern.compile("^(eth\\d+)|(lo)$");
+
+ // Rolling averages for system/process data over 15 mins
+ private static final Map<String, NetworkInterfaceAverages> SYSTEM_AVERAGES = new ConcurrentHashMap<>();
+
+ // poll every minute, keep rolling averages for 15 mins
+ private static final int POLL_INTERVAL = 60;
+ private static final int WINDOW_SIZE_SECONDS = (int) TimeUnit.MINUTES.toSeconds(15);
+ private static final int WINDOW_SIZE = WINDOW_SIZE_SECONDS / POLL_INTERVAL; // 15
+
+ static {
+ // schedule rolling average calculations.
+ MonitoringExecutor.INSTANCE.scheduleAtFixedRate(new RollingAverageCollectionTask(), 1, POLL_INTERVAL, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Ensures that the static initializer has been called.
+ */
+ @SuppressWarnings("EmptyMethod")
+ public static void ensureMonitoring() {
+ // intentionally empty
+ }
+
+ public static Map<String, NetworkInterfaceInfo> systemTotals() {
+ Map<String, NetworkInterfaceInfo> values = SYSTEM.get();
+ return values == null ? Collections.emptyMap() : values;
+ }
+
+ public static Map<String, NetworkInterfaceAverages> systemAverages() {
+ return Collections.unmodifiableMap(SYSTEM_AVERAGES);
+ }
+
+ /**
+ * Task to poll network activity and add to the rolling averages in the enclosing class.
+ */
+ private static final class RollingAverageCollectionTask implements Runnable {
+ private static final BigDecimal POLL_INTERVAL_DECIMAL = BigDecimal.valueOf(POLL_INTERVAL);
+
+ @Override
+ public void run() {
+ Map<String, NetworkInterfaceInfo> values = pollAndDiff(NetworkInterfaceInfo::pollSystem, SYSTEM);
+ if (values != null) {
+ submit(SYSTEM_AVERAGES, values);
+ }
+ }
+
+ /**
+ * Submits the incoming values into the rolling averages map.
+ *
+ * @param values the values
+ */
+ private static void submit(Map<String, NetworkInterfaceAverages> rollingAveragesMap, Map<String, NetworkInterfaceInfo> values) {
+ // ensure all incoming keys are present in the rolling averages map
+ for (String key : values.keySet()) {
+ if (INTERFACES_TO_MONITOR.matcher(key).matches()) {
+ rollingAveragesMap.computeIfAbsent(key, k -> new NetworkInterfaceAverages(WINDOW_SIZE));
+ }
+ }
+
+ // submit a value (0 if unknown) to each rolling average instance in the map
+ for (Map.Entry<String, NetworkInterfaceAverages> entry : rollingAveragesMap.entrySet()) {
+ String interfaceName = entry.getKey();
+ NetworkInterfaceAverages rollingAvgs = entry.getValue();
+
+ NetworkInterfaceInfo info = values.getOrDefault(interfaceName, NetworkInterfaceInfo.ZERO);
+ rollingAvgs.accept(info, RollingAverageCollectionTask::calculateRate);
+ }
+ }
+
+ private static BigDecimal calculateRate(long value) {
+ return BigDecimal.valueOf(value).divide(POLL_INTERVAL_DECIMAL, RoundingMode.HALF_UP);
+ }
+
+ private static Map<String, NetworkInterfaceInfo> pollAndDiff(Supplier<Map<String, NetworkInterfaceInfo>> poller, AtomicReference<Map<String, NetworkInterfaceInfo>> valueReference) {
+ // poll the latest value from the supplier
+ Map<String, NetworkInterfaceInfo> latest = poller.get();
+
+ // update the value ref.
+ // if the previous value was null, and the new value is empty, keep it null
+ Map<String, NetworkInterfaceInfo> previous = valueReference.getAndUpdate(prev -> {
+ if (prev == null && latest.isEmpty()) {
+ return null;
+ } else {
+ return latest;
+ }
+ });
+
+ if (previous == null) {
+ return null;
+ }
+
+ return NetworkInterfaceInfo.difference(latest, previous);
+ }
+ }
+
+}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/monitor/ping/PingStatistics.java b/spark-common/src/main/java/me/lucko/spark/common/monitor/ping/PingStatistics.java
index 8b5b5b3..49fcbe1 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/monitor/ping/PingStatistics.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/monitor/ping/PingStatistics.java
@@ -34,13 +34,14 @@ import java.util.concurrent.TimeUnit;
* Provides statistics for player ping RTT to the server.
*/
public final class PingStatistics implements Runnable, AutoCloseable {
- private static final int WINDOW_SIZE_SECONDS = (int) TimeUnit.MINUTES.toSeconds(15);
private static final int QUERY_RATE_SECONDS = 10;
+ private static final int WINDOW_SIZE_SECONDS = (int) TimeUnit.MINUTES.toSeconds(15); // 900
+ private static final int WINDOW_SIZE = WINDOW_SIZE_SECONDS / QUERY_RATE_SECONDS; // 90
/** The platform function that provides player ping times */
private final PlayerPingProvider provider;
/** Rolling average of the median ping across all players */
- private final RollingAverage rollingAverage = new RollingAverage(WINDOW_SIZE_SECONDS / QUERY_RATE_SECONDS);
+ private final RollingAverage rollingAverage = new RollingAverage(WINDOW_SIZE);
/** The scheduler task that polls pings and calculates the rolling average */
private ScheduledFuture<?> future;
diff --git a/spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java b/spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java
index 5b8d1d4..046def7 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/platform/PlatformStatisticsProvider.java
@@ -26,9 +26,10 @@ import me.lucko.spark.common.monitor.cpu.CpuMonitor;
import me.lucko.spark.common.monitor.disk.DiskUsage;
import me.lucko.spark.common.monitor.memory.GarbageCollectorStatistics;
import me.lucko.spark.common.monitor.memory.MemoryInfo;
+import me.lucko.spark.common.monitor.net.NetworkInterfaceAverages;
+import me.lucko.spark.common.monitor.net.NetworkMonitor;
import me.lucko.spark.common.monitor.ping.PingStatistics;
import me.lucko.spark.common.monitor.tick.TickStatistics;
-import me.lucko.spark.common.util.RollingAverage;
import me.lucko.spark.proto.SparkProtos.PlatformStatistics;
import me.lucko.spark.proto.SparkProtos.SystemStatistics;
@@ -108,6 +109,17 @@ public class PlatformStatisticsProvider {
.build()
));
+ Map<String, NetworkInterfaceAverages> networkInterfaceStats = NetworkMonitor.systemAverages();
+ networkInterfaceStats.forEach((name, statistics) -> builder.putNet(
+ name,
+ SystemStatistics.NetInterface.newBuilder()
+ .setRxBytesPerSecond(statistics.rxBytesPerSecond().toProto())
+ .setRxPacketsPerSecond(statistics.rxPacketsPerSecond().toProto())
+ .setTxBytesPerSecond(statistics.txBytesPerSecond().toProto())
+ .setTxPacketsPerSecond(statistics.txPacketsPerSecond().toProto())
+ .build()
+ ));
+
return builder.build();
}
@@ -149,8 +161,8 @@ public class PlatformStatisticsProvider {
);
if (tickStatistics.isDurationSupported()) {
builder.setMspt(PlatformStatistics.Mspt.newBuilder()
- .setLast1M(rollingAverageValues(tickStatistics.duration1Min()))
- .setLast5M(rollingAverageValues(tickStatistics.duration5Min()))
+ .setLast1M(tickStatistics.duration1Min().toProto())
+ .setLast5M(tickStatistics.duration5Min().toProto())
.build()
);
}
@@ -159,7 +171,7 @@ public class PlatformStatisticsProvider {
PingStatistics pingStatistics = this.platform.getPingStatistics();
if (pingStatistics != null && pingStatistics.getPingAverage().getSamples() != 0) {
builder.setPing(PlatformStatistics.Ping.newBuilder()
- .setLast15M(rollingAverageValues(pingStatistics.getPingAverage()))
+ .setLast15M(pingStatistics.getPingAverage().toProto())
.build()
);
}
@@ -167,14 +179,4 @@ public class PlatformStatisticsProvider {
return builder.build();
}
- private static PlatformStatistics.RollingAverageValues rollingAverageValues(RollingAverage rollingAverage) {
- return PlatformStatistics.RollingAverageValues.newBuilder()
- .setMean(rollingAverage.mean())
- .setMax(rollingAverage.max())
- .setMin(rollingAverage.min())
- .setMedian(rollingAverage.median())
- .setPercentile95(rollingAverage.percentile95th())
- .build();
- }
-
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/platform/serverconfig/AbstractServerConfigProvider.java b/spark-common/src/main/java/me/lucko/spark/common/platform/serverconfig/AbstractServerConfigProvider.java
index d9e8bf4..7e04400 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/platform/serverconfig/AbstractServerConfigProvider.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/platform/serverconfig/AbstractServerConfigProvider.java
@@ -34,7 +34,7 @@ import java.util.Map;
/**
* Abstract implementation of {@link ServerConfigProvider}.
*
- * <p>THis implementation is able to delete hidden paths from
+ * <p>This implementation is able to delete hidden paths from
* the configurations before they are sent to the viewer.</p>
*
* @param <T> the file type
diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java b/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java
index 492d4ea..cbc496d 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/util/FormatUtil.java
@@ -20,6 +20,9 @@
package me.lucko.spark.common.util;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.TextColor;
+
public enum FormatUtil {
;
@@ -37,4 +40,24 @@ public enum FormatUtil {
int sizeIndex = (int) (Math.log(bytes) / Math.log(1024));
return String.format("%.1f", bytes / Math.pow(1024, sizeIndex)) + " " + SIZE_UNITS[sizeIndex];
}
+
+ public static Component formatBytes(long bytes, TextColor color, String suffix) {
+ String value;
+ String unit;
+
+ if (bytes == 0) {
+ value = "0";
+ unit = "KB" + suffix;
+ } else {
+ int sizeIndex = (int) (Math.log(bytes) / Math.log(1024));
+ value = String.format("%.1f", bytes / Math.pow(1024, sizeIndex));
+ unit = SIZE_UNITS[sizeIndex] + suffix;
+ }
+
+ return Component.text()
+ .append(Component.text(value, color))
+ .append(Component.space())
+ .append(Component.text(unit))
+ .build();
+ }
}
diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java b/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java
index 0926ae7..7d688d7 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/util/LinuxProc.java
@@ -44,7 +44,12 @@ public enum LinuxProc {
/**
* Information about the system memory.
*/
- MEMINFO("/proc/meminfo");
+ MEMINFO("/proc/meminfo"),
+
+ /**
+ * Information about the system network usage.
+ */
+ NET_DEV("/proc/net/dev");
private final Path path;
diff --git a/spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java b/spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java
index 57dfdff..65753bc 100644
--- a/spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java
+++ b/spark-common/src/main/java/me/lucko/spark/common/util/RollingAverage.java
@@ -21,6 +21,7 @@
package me.lucko.spark.common.util;
import me.lucko.spark.api.statistic.misc.DoubleAverageInfo;
+import me.lucko.spark.proto.SparkProtos;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -111,4 +112,14 @@ public class RollingAverage implements DoubleAverageInfo {
return sortedSamples[rank].doubleValue();
}
+ public SparkProtos.RollingAverageValues toProto() {
+ return SparkProtos.RollingAverageValues.newBuilder()
+ .setMean(mean())
+ .setMax(max())
+ .setMin(min())
+ .setMedian(median())
+ .setPercentile95(percentile95th())
+ .build();
+ }
+
}