diff --git a/.github/workflows/test-action.yml b/.github/workflows/test-action.yml index debc1f2..fb07f27 100644 --- a/.github/workflows/test-action.yml +++ b/.github/workflows/test-action.yml @@ -109,6 +109,11 @@ jobs: - name: CPU Intensive Activity run: | echo "Starting CPU-intensive activity for 20 seconds..." + echo "Creating 5GB test file to demonstrate disk usage..." + dd if=/dev/zero of=/tmp/test-5gb-file.bin bs=1M count=5120 status=progress + echo "5GB file created at /tmp/test-5gb-file.bin" + ls -lh /tmp/test-5gb-file.bin + node -e " const startTime = Date.now(); const endTime = startTime + 20000; @@ -121,6 +126,10 @@ jobs: } console.log('CPU-intensive activity completed. Count:', count); " + + echo "Removing 5GB test file..." + rm -f /tmp/test-5gb-file.bin + echo "Test file removed." - name: Memory Intensive Activity run: | diff --git a/dist/main/collector.bundle.js b/dist/main/collector.bundle.js index 4f44fc8..4a8bb08 100644 --- a/dist/main/collector.bundle.js +++ b/dist/main/collector.bundle.js @@ -27117,7 +27117,7 @@ var require_filesystem = __commonJS({ var _sunos = _platform === "sunos"; var _fs_speed = {}; var _disk_io = {}; - function fsSize(drive, callback) { + function fsSize2(drive, callback) { if (util.isFunction(drive)) { callback = drive; drive = ""; @@ -27315,7 +27315,7 @@ var require_filesystem = __commonJS({ }); }); } - exports.fsSize = fsSize; + exports.fsSize = fsSize2; function fsOpenFiles(callback) { return new Promise((resolve) => { process.nextTick(() => { @@ -50766,6 +50766,8 @@ function date4(params) { config(en_default()); // src/lib.ts +var bytesPerMB = 1024 * 1024; +var bytesPerGB = 1024 * 1024 * 1024; var cpuLoadPercentageSchema = external_exports.object({ unixTimeMs: external_exports.number(), user: external_exports.number().nonnegative().max(100), @@ -50778,6 +50780,12 @@ var memoryUsageMBSchema = external_exports.object({ free: external_exports.number().nonnegative() }); var memoryUsageMBsSchema = external_exports.array(memoryUsageMBSchema); +var diskUsageGBSchema = external_exports.object({ + unixTimeMs: external_exports.number(), + used: external_exports.number().nonnegative(), + free: external_exports.number().nonnegative() +}); +var diskUsageGBsSchema = external_exports.array(diskUsageGBSchema); var stepMarkerSchema = external_exports.object({ unixTimeMs: external_exports.number(), stepName: external_exports.string(), @@ -50787,6 +50795,7 @@ var stepMarkersSchema = external_exports.array(stepMarkerSchema); var metricsDataSchema = external_exports.object({ cpuLoadPercentages: cpuLoadPercentagesSchema, memoryUsageMBs: memoryUsageMBsSchema, + diskUsageGBs: diskUsageGBsSchema, stepMarkers: stepMarkersSchema }); function getMetricsFilePath() { @@ -50803,7 +50812,7 @@ var Metrics = class { timeoutId = null; stopped = false; constructor() { - this.data = { cpuLoadPercentages: [], memoryUsageMBs: [], stepMarkers: [] }; + this.data = { cpuLoadPercentages: [], memoryUsageMBs: [], diskUsageGBs: [], stepMarkers: [] }; this.filePath = getMetricsFilePath(); this.intervalMs = 5 * 1e3; const intervalSecondsInput = process.env.METRICS_INTERVAL_SECONDS; @@ -50849,13 +50858,20 @@ var Metrics = class { user: currentLoadUser, system: currentLoadSystem }); - const bytesPerMB = 1024 * 1024; const { active, available } = await (0, import_systeminformation.mem)(); this.data.memoryUsageMBs.push({ unixTimeMs, used: active / bytesPerMB, free: available / bytesPerMB }); + const disks = await (0, import_systeminformation.fsSize)(); + const totalUsed = disks.reduce((sum, disk) => sum + disk.used, 0); + const totalAvailable = disks.reduce((sum, disk) => sum + disk.available, 0); + this.data.diskUsageGBs.push({ + unixTimeMs, + used: totalUsed / bytesPerGB, + free: totalAvailable / bytesPerGB + }); await this.writeData(); } catch (error49) { setFailed(error49); diff --git a/dist/post/index.bundle.js b/dist/post/index.bundle.js index a017fb0..ceac856 100644 --- a/dist/post/index.bundle.js +++ b/dist/post/index.bundle.js @@ -61793,7 +61793,7 @@ var require_filesystem = __commonJS({ var _sunos = _platform === "sunos"; var _fs_speed = {}; var _disk_io = {}; - function fsSize(drive, callback) { + function fsSize2(drive, callback) { if (util3.isFunction(drive)) { callback = drive; drive = ""; @@ -61991,7 +61991,7 @@ var require_filesystem = __commonJS({ }); }); } - exports2.fsSize = fsSize; + exports2.fsSize = fsSize2; function fsOpenFiles(callback) { return new Promise((resolve2) => { process.nextTick(() => { @@ -122431,6 +122431,8 @@ ${annotations.join("\n")} // src/lib.ts import { tmpdir } from "node:os"; import { join as join2 } from "node:path"; +var bytesPerMB = 1024 * 1024; +var bytesPerGB = 1024 * 1024 * 1024; var cpuLoadPercentageSchema = external_exports.object({ unixTimeMs: external_exports.number(), user: external_exports.number().nonnegative().max(100), @@ -122443,6 +122445,12 @@ var memoryUsageMBSchema = external_exports.object({ free: external_exports.number().nonnegative() }); var memoryUsageMBsSchema = external_exports.array(memoryUsageMBSchema); +var diskUsageGBSchema = external_exports.object({ + unixTimeMs: external_exports.number(), + used: external_exports.number().nonnegative(), + free: external_exports.number().nonnegative() +}); +var diskUsageGBsSchema = external_exports.array(diskUsageGBSchema); var stepMarkerSchema = external_exports.object({ unixTimeMs: external_exports.number(), stepName: external_exports.string(), @@ -122452,6 +122460,7 @@ var stepMarkersSchema = external_exports.array(stepMarkerSchema); var metricsDataSchema = external_exports.object({ cpuLoadPercentages: cpuLoadPercentagesSchema, memoryUsageMBs: memoryUsageMBsSchema, + diskUsageGBs: diskUsageGBsSchema, stepMarkers: stepMarkersSchema }); function getMetricsFilePath() { @@ -122503,13 +122512,20 @@ async function collectFinalMetrics() { user: currentLoadUser, system: currentLoadSystem }); - const bytesPerMB = 1024 * 1024; const { active, available } = await (0, import_systeminformation.mem)(); metricsData.memoryUsageMBs.push({ unixTimeMs, used: active / bytesPerMB, free: available / bytesPerMB }); + const disks = await (0, import_systeminformation.fsSize)(); + const totalUsed = disks.reduce((sum, disk) => sum + disk.used, 0); + const totalAvailable = disks.reduce((sum, disk) => sum + disk.available, 0); + metricsData.diskUsageGBs.push({ + unixTimeMs, + used: totalUsed / bytesPerGB, + free: totalAvailable / bytesPerGB + }); await writeFile2(filePath, JSON.stringify(metricsData, null, 2), "utf-8"); } catch (error49) { console.warn(`Failed to collect final metrics: ${error49 instanceof Error ? error49.message : String(error49)}`); @@ -122609,6 +122625,31 @@ function render(metricsData, metricsID) { yAxis: { title: "MB" } + }, + { + title: "Disk Usages", + metricsInfoList: [ + { + color: "Cyan", + name: "Free", + data: metricsData.diskUsageGBs.map( + ({ free }) => free + ) + }, + { + color: "Purple", + name: "Used", + data: metricsData.diskUsageGBs.map( + ({ used }) => used + ) + } + ], + times: metricsData.diskUsageGBs.map( + ({ unixTimeMs }) => unixTimeMs + ), + yAxis: { + title: "GB" + } } ]), metricsID, diff --git a/src/lib.ts b/src/lib.ts index d18db7b..813341b 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -2,6 +2,9 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { z } from "zod"; +export const bytesPerMB: number = 1024 * 1024; +export const bytesPerGB: number = 1024 * 1024 * 1024; + export const cpuLoadPercentageSchema = z.object({ unixTimeMs: z.number(), user: z.number().nonnegative().max(100), @@ -14,6 +17,12 @@ export const memoryUsageMBSchema = z.object({ free: z.number().nonnegative(), }); export const memoryUsageMBsSchema = z.array(memoryUsageMBSchema); +export const diskUsageGBSchema = z.object({ + unixTimeMs: z.number(), + used: z.number().nonnegative(), + free: z.number().nonnegative(), +}); +export const diskUsageGBsSchema = z.array(diskUsageGBSchema); export const stepMarkerSchema = z.object({ unixTimeMs: z.number(), stepName: z.string(), @@ -23,6 +32,7 @@ export const stepMarkersSchema = z.array(stepMarkerSchema); export const metricsDataSchema = z.object({ cpuLoadPercentages: cpuLoadPercentagesSchema, memoryUsageMBs: memoryUsageMBsSchema, + diskUsageGBs: diskUsageGBsSchema, stepMarkers: stepMarkersSchema, }); diff --git a/src/main/metrics.test.ts b/src/main/metrics.test.ts index 13d631f..46e7d34 100644 --- a/src/main/metrics.test.ts +++ b/src/main/metrics.test.ts @@ -6,6 +6,7 @@ import { type cpuLoadPercentageSchema, metricsDataSchema, type memoryUsageMBSchema, + type diskUsageGBSchema, } from "../lib.ts"; describe("Metrics", () => { @@ -32,6 +33,29 @@ describe("Metrics", () => { active: 4096 * 1024 * 1024, // 4096 MB in bytes available: 8192 * 1024 * 1024, // 8192 MB in bytes } as Systeminformation.MemData), + fsSize: async (): Promise => + Promise.resolve([ + { + fs: "/dev/root", + type: "ext4", + size: 100 * 1024 * 1024 * 1024, // 100 GB + used: 30 * 1024 * 1024 * 1024, // 30 GB + available: 70 * 1024 * 1024 * 1024, // 70 GB + use: 30, + mount: "/", + rw: true, + }, + { + fs: "/dev/sda1", + type: "ext4", + size: 50 * 1024 * 1024 * 1024, // 50 GB + used: 20 * 1024 * 1024 * 1024, // 20 GB + available: 30 * 1024 * 1024 * 1024, // 30 GB + use: 40, + mount: "/data", + rw: true, + }, + ] as Systeminformation.FsSizeData[]), }, }); @@ -97,9 +121,11 @@ describe("Metrics", () => { assert.ok(data.cpuLoadPercentages); assert.ok(data.memoryUsageMBs); + assert.ok(data.diskUsageGBs); assert.ok(data.stepMarkers); assert.strictEqual(Array.isArray(data.cpuLoadPercentages), true); assert.strictEqual(Array.isArray(data.memoryUsageMBs), true); + assert.strictEqual(Array.isArray(data.diskUsageGBs), true); assert.strictEqual(Array.isArray(data.stepMarkers), true); }); @@ -125,6 +151,12 @@ describe("Metrics", () => { assert.strictEqual(typeof data.memoryUsageMBs[0].unixTimeMs, "number"); assert.ok(data.memoryUsageMBs[0].used !== undefined); assert.ok(data.memoryUsageMBs[0].free !== undefined); + + // Verify disk metrics are collected + assert.ok(data.diskUsageGBs.length > 0); + assert.strictEqual(typeof data.diskUsageGBs[0].unixTimeMs, "number"); + assert.ok(data.diskUsageGBs[0].used !== undefined); + assert.ok(data.diskUsageGBs[0].free !== undefined); }); it("should have correct CPU metrics format", async () => { @@ -168,6 +200,27 @@ describe("Metrics", () => { assert.strictEqual(memData.free, 8192); }); + it("should have correct disk metrics format and conversion", async () => { + const metrics = createMetrics(); + + // Wait for async processing to complete + for (let i = 0; i < 10; i++) { + await new Promise(resolve => queueMicrotask(resolve)); + } + + const diskData: z.TypeOf = JSON.parse( + metrics.get(), + ).diskUsageGBs[0]; + + assert.strictEqual(typeof diskData.unixTimeMs, "number"); + assert.strictEqual(typeof diskData.used, "number"); + assert.strictEqual(typeof diskData.free, "number"); + + // Bytes to GB conversion check (30 GB + 20 GB = 50 GB used, 70 GB + 30 GB = 100 GB free) + assert.strictEqual(diskData.used, 50); + assert.strictEqual(diskData.free, 100); + }); + it("should accumulate metrics data over time", async () => { const metrics = createMetrics(); @@ -181,10 +234,12 @@ describe("Metrics", () => { ); const initialCpuCount: number = initialData.cpuLoadPercentages.length; const initialMemCount: number = initialData.memoryUsageMBs.length; + const initialDiskCount: number = initialData.diskUsageGBs.length; // Verify at least one data point exists initially assert.ok(initialCpuCount > 0); assert.ok(initialMemCount > 0); + assert.ok(initialDiskCount > 0); // Advance time by 5 seconds to trigger next append await mock.timers.tick(5000); @@ -198,12 +253,15 @@ describe("Metrics", () => { ); const updatedCpuCount: number = updatedData.cpuLoadPercentages.length; const updatedMemCount: number = updatedData.memoryUsageMBs.length; + const updatedDiskCount: number = updatedData.diskUsageGBs.length; // Verify data points have increased assert.ok(updatedCpuCount > initialCpuCount); assert.ok(updatedMemCount > initialMemCount); + assert.ok(updatedDiskCount > initialDiskCount); assert.strictEqual(updatedCpuCount, initialCpuCount + 1); assert.strictEqual(updatedMemCount, initialMemCount + 1); + assert.strictEqual(updatedDiskCount, initialDiskCount + 1); }); it("should maintain correct time intervals between data points", async () => { @@ -225,6 +283,7 @@ describe("Metrics", () => { // Verify at least 2 data points exist assert.ok(data.cpuLoadPercentages.length >= 2); assert.ok(data.memoryUsageMBs.length >= 2); + assert.ok(data.diskUsageGBs.length >= 2); // Verify timestamp interval is exactly 5 seconds (5000ms) with mocked timers const cpuTimeDiff: number = @@ -232,10 +291,13 @@ describe("Metrics", () => { data.cpuLoadPercentages[0].unixTimeMs; const memTimeDiff: number = data.memoryUsageMBs[1].unixTimeMs - data.memoryUsageMBs[0].unixTimeMs; + const diskTimeDiff: number = + data.diskUsageGBs[1].unixTimeMs - data.diskUsageGBs[0].unixTimeMs; // With mocked timers and Date, we get exactly 5000ms assert.strictEqual(cpuTimeDiff, 5000); assert.strictEqual(memTimeDiff, 5000); + assert.strictEqual(diskTimeDiff, 5000); }); it("should continue accumulating data for multiple intervals", async () => { @@ -280,5 +342,12 @@ describe("Metrics", () => { finalData.memoryUsageMBs[i - 1].unixTimeMs, ); } + + for (let i = 1; i < finalData.diskUsageGBs.length; i++) { + assert.ok( + finalData.diskUsageGBs[i].unixTimeMs > + finalData.diskUsageGBs[i - 1].unixTimeMs, + ); + } }); }); diff --git a/src/main/metrics.ts b/src/main/metrics.ts index 9c85e15..3f3bc2d 100644 --- a/src/main/metrics.ts +++ b/src/main/metrics.ts @@ -1,9 +1,9 @@ import { writeFile, mkdir } from "node:fs/promises"; import { dirname } from "node:path"; import { setFailed } from "@actions/core"; -import { currentLoad, mem } from "systeminformation"; +import { currentLoad, mem, fsSize } from "systeminformation"; import type { z } from "zod"; -import { metricsDataSchema, getMetricsFilePath } from "../lib.ts"; +import { metricsDataSchema, getMetricsFilePath, bytesPerMB, bytesPerGB } from "../lib.ts"; export class Metrics { private readonly data: z.TypeOf; @@ -13,7 +13,7 @@ export class Metrics { private stopped: boolean = false; constructor() { - this.data = { cpuLoadPercentages: [], memoryUsageMBs: [], stepMarkers: [] }; + this.data = { cpuLoadPercentages: [], memoryUsageMBs: [], diskUsageGBs: [], stepMarkers: [] }; this.filePath = getMetricsFilePath(); this.intervalMs = 5 * 1000; @@ -72,7 +72,6 @@ export class Metrics { system: currentLoadSystem, }); - const bytesPerMB: number = 1024 * 1024; const { active, available }: { active: number; available: number } = await mem(); this.data.memoryUsageMBs.push({ @@ -81,6 +80,16 @@ export class Metrics { free: available / bytesPerMB, }); + const disks = await fsSize(); + // Sum all disks to get total disk usage + const totalUsed = disks.reduce((sum, disk) => sum + disk.used, 0); + const totalAvailable = disks.reduce((sum, disk) => sum + disk.available, 0); + this.data.diskUsageGBs.push({ + unixTimeMs, + used: totalUsed / bytesPerGB, + free: totalAvailable / bytesPerGB, + }); + // Write to file after collecting metrics await this.writeData(); } catch (error) { diff --git a/src/post/lib.test.ts b/src/post/lib.test.ts index 9f10ba5..3dd56c1 100644 --- a/src/post/lib.test.ts +++ b/src/post/lib.test.ts @@ -18,6 +18,10 @@ const sampleMetricsData: z.TypeOf = { { unixTimeMs: 1704067200000, used: 4096, free: 8192 }, { unixTimeMs: 1704067205000, used: 4200, free: 8000 }, ], + diskUsageGBs: [ + { unixTimeMs: 1704067200000, used: 50, free: 100 }, + { unixTimeMs: 1704067205000, used: 55, free: 95 }, + ], stepMarkers: [ { unixTimeMs: 1704067199000, stepName: "Test Step", status: "start" as const }, { unixTimeMs: 1704067206000, stepName: "Test Step", status: "end" as const }, @@ -36,12 +40,14 @@ describe("render", () => { // Verify rendered result contains expected content assert.ok(result.includes("CPU Loads")); assert.ok(result.includes("Memory Usages")); + assert.ok(result.includes("Disk Usages")); }); it("should handle empty metrics data", () => { const metricsData: z.TypeOf = { cpuLoadPercentages: [], memoryUsageMBs: [], + diskUsageGBs: [], stepMarkers: [ { unixTimeMs: 1704067199000, stepName: "Test Step", status: "start" as const }, { unixTimeMs: 1704067206000, stepName: "Test Step", status: "end" as const }, @@ -64,6 +70,10 @@ describe("render", () => { { unixTimeMs: 1704067200000, used: 4000, free: 8000 }, { unixTimeMs: 1704067205000, used: 4100, free: 7900 }, ], + diskUsageGBs: [ + { unixTimeMs: 1704067200000, used: 50, free: 100 }, + { unixTimeMs: 1704067205000, used: 55, free: 95 }, + ], stepMarkers: [ { unixTimeMs: 1704067199000, stepName: "Test Step", status: "start" as const }, { unixTimeMs: 1704067206000, stepName: "Test Step", status: "end" as const }, @@ -83,6 +93,10 @@ describe("render", () => { { unixTimeMs: 1704067200000, used: 5000, free: 10000 }, { unixTimeMs: 1704067205000, used: 5500, free: 9500 }, ], + diskUsageGBs: [ + { unixTimeMs: 1704067200000, used: 50, free: 100 }, + { unixTimeMs: 1704067205000, used: 55, free: 95 }, + ], stepMarkers: [ { unixTimeMs: 1704067199000, stepName: "Test Step", status: "start" as const }, { unixTimeMs: 1704067206000, stepName: "Test Step", status: "end" as const }, diff --git a/src/post/lib.ts b/src/post/lib.ts index a1db03d..c618057 100644 --- a/src/post/lib.ts +++ b/src/post/lib.ts @@ -2,9 +2,9 @@ import { readFile, writeFile } from "node:fs/promises"; import { z } from "zod"; import { getInput } from "@actions/core"; import { context, getOctokit } from "@actions/github"; -import { currentLoad, mem } from "systeminformation"; +import { currentLoad, mem, fsSize } from "systeminformation"; import { Renderer } from "./renderer.ts"; -import { metricsDataSchema, getMetricsFilePath, stepMarkerSchema } from "../lib.ts"; +import { metricsDataSchema, getMetricsFilePath, stepMarkerSchema, bytesPerMB, bytesPerGB } from "../lib.ts"; export const metricsInfoSchema = z.object({ color: z.string(), @@ -60,7 +60,6 @@ export async function collectFinalMetrics(): Promise { system: currentLoadSystem, }); - const bytesPerMB: number = 1024 * 1024; const { active, available }: { active: number; available: number } = await mem(); metricsData.memoryUsageMBs.push({ @@ -69,6 +68,16 @@ export async function collectFinalMetrics(): Promise { free: available / bytesPerMB, }); + const disks = await fsSize(); + // Sum all disks to get total disk usage + const totalUsed = disks.reduce((sum, disk) => sum + disk.used, 0); + const totalAvailable = disks.reduce((sum, disk) => sum + disk.available, 0); + metricsData.diskUsageGBs.push({ + unixTimeMs, + used: totalUsed / bytesPerGB, + free: totalAvailable / bytesPerGB, + }); + // Write updated metrics back to file await writeFile(filePath, JSON.stringify(metricsData, null, 2), "utf-8"); } catch (error) { @@ -184,6 +193,31 @@ export function render( title: "MB", }, }, + { + title: "Disk Usages", + metricsInfoList: [ + { + color: "Cyan", + name: "Free", + data: metricsData.diskUsageGBs.map( + ({ free }: { free: number }): number => free, + ), + }, + { + color: "Purple", + name: "Used", + data: metricsData.diskUsageGBs.map( + ({ used }: { used: number }): number => used, + ), + }, + ], + times: metricsData.diskUsageGBs.map( + ({ unixTimeMs }: { unixTimeMs: number }): number => unixTimeMs, + ), + yAxis: { + title: "GB", + }, + }, ]), metricsID, metricsData.stepMarkers,