Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
681cb29686 |
@@ -0,0 +1,172 @@
|
||||
package juloo.keyboard2;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
/** Host load history behind the space bar ribbon.
|
||||
*
|
||||
* Two rules, both Harley's, and they are the whole design:
|
||||
* - failure is quiet. Offline, VPN down, Utumno off, junk payload or a timeout all end as
|
||||
* "no data" - never a crash, and never a stale trace wearing a live face.
|
||||
* - parsing happens on the fetch thread, so the draw path can neither block nor throw.
|
||||
*/
|
||||
public final class HostGraph
|
||||
{
|
||||
/** Payload series order. io leads: a stalled disk is the thing worth seeing. */
|
||||
public static final String[] SERIES = { "io", "cpu" };
|
||||
/** A sample older than this is not worth drawing as if it were now. */
|
||||
public static final long MAX_AGE_MS = 15000L;
|
||||
private static final int CONNECT_TIMEOUT_MS = 2500;
|
||||
private static final int READ_TIMEOUT_MS = 2500;
|
||||
private static final long MIN_INTERVAL_MS = 2000L;
|
||||
private static final int DEFAULT_N = 60;
|
||||
|
||||
private static final Object _lock = new Object();
|
||||
private static float[][] _values = new float[SERIES.length][];
|
||||
private static long _fetchedAt = 0L;
|
||||
private static long _lastAttempt = 0L;
|
||||
private static boolean _inFlight = false;
|
||||
|
||||
private HostGraph() {}
|
||||
|
||||
/** True only when there is something honest to draw: data, and data that is still current. */
|
||||
public static boolean hasData()
|
||||
{
|
||||
synchronized (_lock)
|
||||
{
|
||||
if (_values[0] == null || _values[0].length == 0)
|
||||
return false;
|
||||
return System.currentTimeMillis() - _fetchedAt < MAX_AGE_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy of one series; empty when there is nothing to draw. Safe on the draw path. */
|
||||
public static float[] series(int index)
|
||||
{
|
||||
synchronized (_lock)
|
||||
{
|
||||
float[] v = (index >= 0 && index < _values.length) ? _values[index] : null;
|
||||
return v == null ? new float[0] : v.clone();
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch at most every couple of seconds; call freely while the keyboard is visible. */
|
||||
public static void fetchIfNeeded(String baseUrl, String token)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
synchronized (_lock)
|
||||
{
|
||||
if (_inFlight || now - _lastAttempt < MIN_INTERVAL_MS)
|
||||
return;
|
||||
_inFlight = true;
|
||||
_lastAttempt = now;
|
||||
}
|
||||
Thread worker = new Thread(() -> {
|
||||
try
|
||||
{
|
||||
run(baseUrl, token);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
forget();
|
||||
}
|
||||
finally
|
||||
{
|
||||
synchronized (_lock) { _inFlight = false; }
|
||||
}
|
||||
}, "host-graph");
|
||||
worker.setDaemon(true);
|
||||
worker.start();
|
||||
}
|
||||
|
||||
private static void forget()
|
||||
{
|
||||
synchronized (_lock)
|
||||
{
|
||||
_values = new float[SERIES.length][];
|
||||
_fetchedAt = 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private static void run(String baseUrl, String token)
|
||||
{
|
||||
HttpURLConnection connection = null;
|
||||
try
|
||||
{
|
||||
String url = baseUrl + "/graph?n=" + DEFAULT_N;
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
if (token != null && !token.isEmpty())
|
||||
connection.setRequestProperty("Authorization", "Bearer " + token);
|
||||
StringBuilder body = new StringBuilder();
|
||||
try (InputStream input = connection.getInputStream();
|
||||
InputStreamReader reader = new InputStreamReader(input, "UTF-8"))
|
||||
{
|
||||
char[] buf = new char[4096];
|
||||
int n;
|
||||
while ((n = reader.read(buf)) > 0)
|
||||
body.append(buf, 0, n);
|
||||
}
|
||||
float[][] parsed = parse(body.toString());
|
||||
if (parsed == null)
|
||||
{
|
||||
forget();
|
||||
return;
|
||||
}
|
||||
synchronized (_lock)
|
||||
{
|
||||
_values = parsed;
|
||||
_fetchedAt = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// No VPN, no route, host off, refused, unreadable: one answer for all of them.
|
||||
forget();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (connection != null)
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses the payload, or returns null when it cannot be trusted. Pure, therefore tested. */
|
||||
static float[][] parse(String json)
|
||||
{
|
||||
if (json == null)
|
||||
return null;
|
||||
float[][] out = new float[SERIES.length][];
|
||||
for (int i = 0; i < SERIES.length; i++)
|
||||
{
|
||||
int nameAt = json.indexOf("\"" + SERIES[i] + "\"");
|
||||
if (nameAt < 0)
|
||||
return null; // a series we cannot read is no data at all
|
||||
int open = json.indexOf('[', nameAt);
|
||||
int close = open < 0 ? -1 : json.indexOf(']', open);
|
||||
if (open < 0 || close < 0)
|
||||
return null;
|
||||
String[] parts = json.substring(open + 1, close).split(",");
|
||||
float[] values = new float[parts.length];
|
||||
for (int j = 0; j < parts.length; j++)
|
||||
{
|
||||
try
|
||||
{
|
||||
values[j] = Float.parseFloat(parts[j].trim());
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (values.length == 0)
|
||||
return null;
|
||||
out[i] = values;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package juloo.keyboard2;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
/** The ribbon must fail to nothing, never to a lie or a crash. */
|
||||
public class HostGraphTest
|
||||
{
|
||||
private static final String GOOD = "{\"ts\": \"2026-09-27T18:30:32+00:00\", \"host\": \"utumno\", "
|
||||
+ "\"version\": \"0.1.0\", \"window_s\": 6, \"series\": ["
|
||||
+ "{\"name\": \"io\", \"unit\": \"%\", \"values\": [0.0, 0.0, 13.6, 2.9, 6.0]}, "
|
||||
+ "{\"name\": \"cpu\", \"unit\": \"%\", \"values\": [0.0, 0.0, 0.1, 0.9, 0.2]}]}";
|
||||
|
||||
@Test
|
||||
public void realPayloadParses()
|
||||
{
|
||||
float[][] parsed = HostGraph.parse(GOOD);
|
||||
assertNotNull(parsed);
|
||||
assertEquals(2, parsed.length);
|
||||
assertEquals(5, parsed[0].length);
|
||||
assertEquals(13.6f, parsed[0][2], 0.001f);
|
||||
assertEquals(0.2f, parsed[1][4], 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void garbageIsNoData()
|
||||
{
|
||||
assertNull(HostGraph.parse("not json at all"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullIsNoData()
|
||||
{
|
||||
assertNull(HostGraph.parse(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aMissingSeriesIsNoData()
|
||||
{
|
||||
assertNull(HostGraph.parse("{\"series\": [{\"name\": \"io\", \"values\": [1.0]}]}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyValuesAreNoData()
|
||||
{
|
||||
assertNull(HostGraph.parse("{\"series\": [{\"name\": \"io\", \"values\": []}, "
|
||||
+ "{\"name\": \"cpu\", \"values\": [1.0]}]}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aJunkNumberIsNoData()
|
||||
{
|
||||
assertNull(HostGraph.parse("{\"series\": [{\"name\": \"io\", \"values\": [1.0, abc]}, "
|
||||
+ "{\"name\": \"cpu\", \"values\": [1.0]}]}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aShortWindowStillParses()
|
||||
{
|
||||
float[][] parsed = HostGraph.parse("{\"series\": [{\"name\": \"io\", \"values\": [3.5]}, "
|
||||
+ "{\"name\": \"cpu\", \"values\": [0.5]}]}");
|
||||
assertNotNull(parsed);
|
||||
assertEquals(1, parsed[0].length);
|
||||
assertEquals(3.5f, parsed[0][0], 0.001f);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nothingIsDrawnBeforeAnyDataArrives()
|
||||
{
|
||||
assertEquals(0, HostGraph.series(0).length);
|
||||
assertEquals(false, HostGraph.hasData());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user