Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

experimental; pub sub metrics; dynamic poll interval #11

Merged
merged 4 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 88 additions & 24 deletions main/background.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import path from "path";
import { app, ipcMain, shell } from "electron";
import { app, ipcMain, shell, BrowserWindow } from "electron";
import serve from "electron-serve";
import { createWindow } from "./helpers";
import parsePrometheusTextFormat from "parse-prometheus-text-format";
import { MinorParser } from "./types/Minor";
import { Metrics } from "../types/metrics";
import { createWindow } from "./helpers";
import { PrometheusMetricParser } from "./types/prometheus";
import { SetMetricsStateActionPayload } from "../types/metrics";

const isProd = process.env.NODE_ENV === "production";

Expand All @@ -14,10 +14,12 @@ if (isProd) {
app.setPath("userData", `${app.getPath("userData")} (development)`);
}

let mainWindow: BrowserWindow;

(async () => {
await app.whenReady();

const mainWindow = createWindow("main", {
mainWindow = createWindow("main", {
width: 1000,
height: 600,
webPreferences: {
Expand All @@ -34,34 +36,35 @@ if (isProd) {
}
})();

let isAlive = true;
app.on("window-all-closed", () => {
isAlive = false;
app.quit();
});

ipcMain.on("message", async (event, arg) => {
event.reply("message", `${arg} World!`);
});

function metric_string_parse(item): number | null {
function metricStringParse(item: PrometheusMetricParser | undefined): number | null {
if (!item) return null;
return +item.metrics[0].value;
}

// better dev quality, temp solution
async function getMetrics(): Promise<Metrics> {
async function getMetrics(): Promise<SetMetricsStateActionPayload> {
console.log("DEBUG: getMetrics start");
const res = await fetch("http://testnet-3.arweave.net:1984/metrics");
const data = await res.text();

const parsed: MinorParser[] = parsePrometheusTextFormat(data);
const parsed: PrometheusMetricParser[] = parsePrometheusTextFormat(data) || [];
let dataUnpacked = 0;
let dataPacked = 0;
let storageAvailable = 0;
const packing_item = parsed.find(
(item: MinorParser) => item.name === "v2_index_data_size_by_packing",
const packingItem = parsed.find(
(item: PrometheusMetricParser) => item.name === "v2_index_data_size_by_packing",
);
if (packing_item) {
packing_item.metrics.forEach((item) => {
if (packingItem) {
packingItem.metrics.forEach((item) => {
// unpacked storage modules are not involved in mining
if (item.labels.packing == "unpacked") {
dataUnpacked += +item.value;
Expand All @@ -74,15 +77,15 @@ async function getMetrics(): Promise<Metrics> {
}
});
}
const hashRate = metric_string_parse(
parsed.find((item: MinorParser) => item.name === "average_network_hash_rate"),
const hashRate = metricStringParse(
parsed.find((item: PrometheusMetricParser) => item.name === "average_network_hash_rate"),
);
const earnings = metric_string_parse(
parsed.find((item: MinorParser) => item.name === "average_block_reward"),
const earnings = metricStringParse(
parsed.find((item: PrometheusMetricParser) => item.name === "average_block_reward"),
);

const vdf_step_time_milliseconds_bucket = parsed.find(
(item: MinorParser) => item.name === "vdf_step_time_milliseconds",
(item: PrometheusMetricParser) => item.name === "vdf_step_time_milliseconds",
);
let vdfTimeLowerBound: number | null = null;
if (vdf_step_time_milliseconds_bucket) {
Expand All @@ -95,8 +98,8 @@ async function getMetrics(): Promise<Metrics> {
}
}
}
const weaveSize = metric_string_parse(
parsed.find((item: MinorParser) => item.name === "weave_size"),
const weaveSize = metricStringParse(
parsed.find((item: PrometheusMetricParser) => item.name === "weave_size"),
);
console.log("DEBUG: getMetrics complete");
return {
Expand All @@ -110,11 +113,72 @@ async function getMetrics(): Promise<Metrics> {
};
}

const cached_metrics: Promise<Metrics> = getMetrics();
ipcMain.on("metrics", async (event) => {
event.reply("metrics", await cached_metrics);
// TODO make generic function for creating pub+sub endpoints
// TODO make class for subscription management
let cachedMetrics: SetMetricsStateActionPayload | null = null;
let cachedMetricsStr = "";
// TODO list of webContents
// let cachedMetricsSubList = [];
let cachedMetricsIsSubActive = false;
let cachedMetricsTimeout: NodeJS.Timeout | null = null;
let cachedMetricsUpdateInProgress = false;

async function cachedMetricsUpdate() {
try {
cachedMetricsUpdateInProgress = true;
cachedMetrics = await getMetrics();
} catch (err) {
console.error(err);
}
cachedMetricsUpdateInProgress = false;
}
function cachedMetricsPush() {
const newCachedMetricsStr = JSON.stringify(cachedMetrics);
if (cachedMetricsStr !== newCachedMetricsStr && mainWindow) {
cachedMetricsStr = newCachedMetricsStr;
mainWindow.webContents.send("metricsPush", cachedMetrics);
}
}

async function cachedMetricsUpdatePing() {
if (!isAlive) return;
if (cachedMetricsUpdateInProgress) return;
if (cachedMetricsTimeout) {
clearTimeout(cachedMetricsTimeout);
cachedMetricsTimeout = null;
// extra push fast. Needed on initial subscription
cachedMetricsPush();
await cachedMetricsUpdate();
cachedMetricsPush();
}
// extra check needed
if (!isAlive) return;
// prod active value 1000
// debug active value 10000 (do not kill testnet node)
const delay = cachedMetricsIsSubActive ? 10000 : 60000;
cachedMetricsTimeout = setTimeout(async () => {
// extra check needed
if (!isAlive) return;
cachedMetricsTimeout = null;
await cachedMetricsUpdate();
cachedMetricsPush();
cachedMetricsUpdatePing();
}, delay);
}

(async function () {
await cachedMetricsUpdate();
cachedMetricsUpdatePing();
})();

ipcMain.on("metricsSub", async () => {
cachedMetricsIsSubActive = true;
cachedMetricsUpdatePing();
});
ipcMain.on("metricsUnsub", async () => {
cachedMetricsIsSubActive = false;
});

ipcMain.on("open-url", async (event, arg) => {
ipcMain.on("open-url", async (_event, arg) => {
shell.openExternal(arg);
});
15 changes: 9 additions & 6 deletions main/helpers/create-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ export const createWindow = (
const key = "window-state";
const name = `window-state-${windowName}`;
const store = new Store<Rectangle>({ name });
const defaultSize = {
width: options.width,
height: options.height,
const defaultSize: Rectangle = {
width: options.width || 800,
height: options.height || 600,
x: 0,
y: 0,
};
let state = {};

let state: Rectangle = { x: 0, y: 0, width: 0, height: 0 };

const restore = () => store.get(key, defaultSize);

Expand All @@ -34,7 +37,7 @@ export const createWindow = (
};
};

const windowWithinBounds = (windowState, bounds) => {
const windowWithinBounds = (windowState: Rectangle, bounds: Rectangle) => {
return (
windowState.x >= bounds.x &&
windowState.y >= bounds.y &&
Expand All @@ -51,7 +54,7 @@ export const createWindow = (
});
};

const ensureVisibleOnSomeDisplay = (windowState) => {
const ensureVisibleOnSomeDisplay = (windowState: Rectangle) => {
const visible = screen.getAllDisplays().some((display) => {
return windowWithinBounds(windowState, display.bounds);
});
Expand Down
23 changes: 13 additions & 10 deletions main/preload.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";
import { Metrics } from "../types/metrics";
import { SetMetricsStateActionPayload } from "../types/metrics";

ipcRenderer.on("metricsPush", (_event, msg) => {
console.log("DEBUG metricsPush FE", msg);
});
const handler = {
send(channel: string, value: unknown) {
ipcRenderer.send(channel, value);
},
requestMetrics: function (): Promise<Metrics> {
return new Promise((resolve: (res: Metrics) => void) => {
const subscription = (_event: IpcRendererEvent, res: Metrics) => {
ipcRenderer.off("metrics", subscription);
resolve(res);
};
ipcRenderer.on("metrics", subscription);
ipcRenderer.send("metrics", {});
});
metricsSub: (handler: (_event: unknown, res: SetMetricsStateActionPayload) => void) => {
console.log("DEBUG metricsSub FE");
ipcRenderer.on("metricsPush", handler);
ipcRenderer.send("metricsSub", {});
},
metricsUnsub: (handler: (_event: unknown, res: SetMetricsStateActionPayload) => void) => {
console.log("DEBUG metricsUnsub FE");
ipcRenderer.off("metricsPush", handler);
ipcRenderer.send("metricsUnsub", {});
},
on(channel: string, callback: (...args: unknown[]) => void) {
const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => callback(...args);
Expand Down
2 changes: 1 addition & 1 deletion main/types/Minor.ts → main/types/prometheus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type MinorParser = {
export type PrometheusMetricParser = {
name: string;
help: string;
type: string;
Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
},
"devDependencies": {
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.15",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"autoprefixer": "^10.4.16",
Expand Down
23 changes: 10 additions & 13 deletions renderer/components/Charts/DataRelated.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import { BottomArrow, TopArrow } from "./Arrows";
import { useDataPacked, useStorageAvailable, useWeaveSize } from "../../store/metricsSliceHooks";

interface DataRelatedChartProps {
dataPacked: number | null;
storageAvailable: number | null;
weaveSize: number | null;
}
export default function DataRelatedChart() {
const { dataPacked } = useDataPacked();
const { storageAvailable } = useStorageAvailable();
const { weaveSize } = useWeaveSize();

export default function DataRelatedChart({
dataPacked,
storageAvailable,
weaveSize,
}: DataRelatedChartProps) {
// NOTE maybe this component should pick all stuff from storage directly
return (
<div className="w-96 h-20 flex items-center mt-20">
Expand All @@ -20,7 +15,7 @@ export default function DataRelatedChart({
width: "2%",
}}
>
<TopArrow value={dataPacked} color="#7BF05E" />
{typeof dataPacked === "number" && <TopArrow value={dataPacked} color="#7BF05E" />}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's simplier
{dataPacked != null && }
Both checks for null and undefined

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is indeed a clever trick to catch both, I didn't know it was possible. But since many years, I've avoided the use of == and != in favor of === and !==, I believe eslint will also give an error if you use the the older form, but I guess there are some gems that we lose by always using === / !==. (e.g. > "0" == 0 is true)

</div>

<div
Expand All @@ -29,11 +24,13 @@ export default function DataRelatedChart({
width: "4%",
}}
>
<BottomArrow value={storageAvailable} color="#1D2988" />
{typeof storageAvailable === "number" && (
<BottomArrow value={storageAvailable} color="#1D2988" />
)}
</div>

<div className="w-full bg-[#A7A7A7] hover:bg-[#989797] h-full cursor-pointer relative group">
<TopArrow value={weaveSize} color="#A7A7A7" />
{typeof weaveSize === "number" && <TopArrow value={weaveSize} color="#A7A7A7" />}
</div>
</div>
);
Expand Down
Loading