Skip to content
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
2 changes: 1 addition & 1 deletion .drone.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ local bootstrap = '25.02';
local nginx = '1.24.0';
local python = '3.12-slim-bookworm';
local alpine = '3.21';
local visual_diff_skip_build = '3076';
local visual_diff_skip_build = '3084';

local build(arch, testUI) = [{
kind: 'pipeline',
Expand Down
9 changes: 8 additions & 1 deletion web/platform/src/stub/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,8 @@ export function mock () {
const stubHealth = {
tick: 0,
cpu: { user: 1000000, nice: 100, system: 200000, idle: 5000000, iowait: 30000, irq: 0, softirq: 10000, steal: 0 },
swapIn: 4000000,
swapOut: 5000000,
net: [
{ name: 'eth0', rx_bytes: 50000000, tx_bytes: 20000000 },
{ name: 'wlan0', rx_bytes: 1000000, tx_bytes: 500000 }
Expand All @@ -846,6 +848,9 @@ export function mock () {
d.sectors_written += Math.round(200 + 4000 * Math.random())
})
const memUsed = 1500000 + Math.round(800000 * Math.sin(stubHealth.tick / 12))
const swapPages = Math.max(0, Math.round(400 * Math.sin(stubHealth.tick / 9)))
stubHealth.swapIn += swapPages
stubHealth.swapOut += Math.round(swapPages / 2)
const data = {
cpu: { ...stubHealth.cpu },
memory: {
Expand All @@ -855,7 +860,9 @@ export function mock () {
buffers_kb: 50000,
cached_kb: 900000,
swap_total_kb: 2097148,
swap_free_kb: 2097148 - 600000 - Math.round(300000 * Math.sin(stubHealth.tick / 7))
swap_free_kb: 2097148 - 600000 - Math.round(300000 * Math.sin(stubHealth.tick / 7)),
swap_in_pages: stubHealth.swapIn,
swap_out_pages: stubHealth.swapOut
},
disks: stubHealth.disks.map(d => ({ ...d })),
mounts: [
Expand Down
10 changes: 9 additions & 1 deletion web/platform/src/views/Health.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

<div class="setline" v-if="swapTotalMb > 0">
<h3>{{ $t('health.swap') }} — {{ swapUsedMb }} / {{ swapTotalMb }} MB</h3>
<s-progress :percentage="swapPct" :stroke-width="14" :show-text="false" :status="pctStatus(swapPct)" data-testid="health-swap-bar" />
<s-progress :percentage="swapPct" :stroke-width="14" :show-text="false" :status="swapStatus" data-testid="health-swap-bar" />
<div class="muted" data-testid="health-swap-rate">{{ $t('health.swapIn') }} {{ swapRate.inKBs }} · {{ $t('health.swapOut') }} {{ swapRate.outKBs }} KB/s</div>
</div>
</div>
Expand Down Expand Up @@ -78,6 +78,8 @@ import axios from 'axios'

const METRICS_INTERVAL_MS = 2000
const EVENTS_INTERVAL_MS = 10000
const SWAP_ACTIVE_KBS = 256
const SWAP_BUSY_KBS = 2048

export default {
name: 'Health',
Expand Down Expand Up @@ -109,6 +111,12 @@ export default {
const t = this.metrics.memory.swap_total_kb || 0
if (!t) return 0
return ((t - (this.metrics.memory.swap_free_kb || 0)) / t) * 100
},
swapStatus () {
const rate = this.swapRate.inKBs + this.swapRate.outKBs
if (rate >= SWAP_BUSY_KBS) return 'exception'
if (rate >= SWAP_ACTIVE_KBS) return 'warning'
return 'success'
}
},
methods: {
Expand Down
87 changes: 87 additions & 0 deletions web/platform/tests/unit/Health.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { mount } from '@vue/test-utils'
import axios from 'axios'
import MockAdapter from 'axios-mock-adapter'
import flushPromises from 'flush-promises'
import Health from '../../src/views/Health.vue'

jest.setTimeout(30000)

function metrics (swapFreeKb, swapInPages, swapOutPages) {
return {
cpu: { user: 1000, nice: 0, system: 100, idle: 9000, iowait: 0, irq: 0, softirq: 0, steal: 0 },
memory: {
total_kb: 8052952,
available_kb: 1605392,
free_kb: 265000,
buffers_kb: 240000,
cached_kb: 2054000,
swap_total_kb: 3073016,
swap_free_kb: swapFreeKb,
swap_in_pages: swapInPages,
swap_out_pages: swapOutPages
},
disks: [],
mounts: [],
net: []
}
}

async function mountHealth (samples) {
const mock = new MockAdapter(axios)
let index = 0
mock.onGet('/rest/health/metrics').reply(() => {
const sample = samples[Math.min(index, samples.length - 1)]
index++
return [200, { success: true, data: sample }]
})
mock.onGet(/\/rest\/health\/events/).reply(200, { success: true, data: [] })
const wrapper = mount(Health, { attachTo: document.body })
await flushPromises()
for (let i = 0; i < samples.length; i++) {
wrapper.vm.fetchMetrics()
await flushPromises()
}
return { wrapper, mock }
}

test('full but idle swap is not flagged', async () => {
const { wrapper, mock } = await mountHealth([
metrics(0, 1000000, 2000000),
metrics(0, 1000000, 2000000)
])
expect(Math.round(wrapper.vm.swapPct)).toBe(100)
expect(wrapper.vm.swapRate).toEqual({ inKBs: 0, outKBs: 0 })
expect(wrapper.vm.swapStatus).toBe('success')
wrapper.unmount()
mock.restore()
})

test('sustained paging is flagged even when swap is mostly free', async () => {
const { wrapper, mock } = await mountHealth([
metrics(3000000, 1000000, 2000000),
metrics(3000000, 1002000, 2002000)
])
expect(wrapper.vm.swapPct).toBeLessThan(10)
expect(wrapper.vm.swapStatus).toBe('exception')
wrapper.unmount()
mock.restore()
})

test('light paging warns', async () => {
const { wrapper, mock } = await mountHealth([
metrics(1500000, 1000000, 2000000),
metrics(1500000, 1000200, 2000000)
])
expect(wrapper.vm.swapStatus).toBe('warning')
wrapper.unmount()
mock.restore()
})

test('memory and disk bars still key off fill', async () => {
const { wrapper, mock } = await mountHealth([metrics(0, 0, 0)])
expect(wrapper.vm.pctStatus(95)).toBe('exception')
expect(wrapper.vm.pctStatus(80)).toBe('warning')
expect(wrapper.vm.pctStatus(10)).toBe('success')
wrapper.unmount()
mock.restore()
})