Files
peardata/server/services/collectors/dmcache.js
T
Raven Scott ac4718cb66
CI / test (push) Failing after 4s
Release rolling / release (push) Successful in 7m14s
Further CPU Experimental Changes
2026-07-21 12:17:06 -04:00

149 lines
3.8 KiB
JavaScript

/**
* Device-mapper cache stats (dm-cache / dm-cache target).
*
* Auto when dm cache devices are present.
* Disable: PEARDATA_DMCACHE=0
*
* Charts: dmcache.stats
*/
import fs from 'fs'
import path from 'path'
import { CollectorPlugin } from './plugin.js'
import { registerChart } from '../../../shared/metrics.js'
import { readFileBuf } from '../../utils/fd-cache.js'
import { execFile } from '../../utils/exec.js'
import logger from '../../utils/logger.js'
const log = logger.child('dmcache')
const CHART_STATS = {
id: 'dmcache.stats',
name: 'dmcache.stats',
context: 'dmcache.stats',
title: 'dm-cache stats',
units: 'count',
family: 'dmcache',
chartType: 'line',
priority: 4340,
plugin: 'dmcache',
dimensions: [
{ id: 'hits', name: 'hits', algorithm: 'absolute' },
{ id: 'misses', name: 'misses', algorithm: 'absolute' },
],
}
export function isDmcacheEnabled() {
const v = process.env.PEARDATA_DMCACHE
if (v === '0' || v === 'off' || v === 'false') return false
if (v === '1' || v === 'on' || v === 'true') return true
return discoverDmCacheDevices().length > 0
}
function readFile(p) {
return readFileBuf(p)
}
/**
* @returns {string[]}
*/
export function discoverDmCacheDevices() {
/** @type {string[]} */
const found = []
const blockRoot = '/sys/block'
if (!fs.existsSync(blockRoot)) return found
for (const name of fs.readdirSync(blockRoot)) {
if (!name.startsWith('dm-')) continue
const dmName = readFile(path.join(blockRoot, name, 'dm', 'name'))
if (!dmName) continue
const lower = dmName.toLowerCase()
if (lower.includes('cache') || lower.includes('dmcache')) {
found.push(name)
}
}
return found
}
/**
* @param {string} output
* @returns {{ hits: number|null, misses: number|null }}
*/
export function parseDmsetupStatus(output) {
let hits = null
let misses = null
for (const line of String(output || '').split('\n')) {
const h = line.match(/(\d+)\s+hits?/i)
const m = line.match(/(\d+)\s+miss(?:es)?/i)
if (h) hits = (hits ?? 0) + Number(h[1])
if (m) misses = (misses ?? 0) + Number(m[1])
}
return { hits, misses }
}
/**
* @returns {Promise<{ hits: number|null, misses: number|null }>}
*/
export async function collectDmcacheStats() {
const devices = discoverDmCacheDevices()
if (!devices.length) return { hits: null, misses: null }
try {
const { stdout } = await execFile('dmsetup', ['status'], { timeout: 3000 })
const parsed = parseDmsetupStatus(stdout)
if (parsed.hits != null || parsed.misses != null) return parsed
} catch {
// fall through to sysfs
}
let hits = null
let misses = null
for (const dev of devices) {
const statsDir = path.join('/sys/block', dev, 'dm', 'stats')
const raw = readFile(statsDir) || readFile(path.join('/sys/block', dev, 'stat'))
if (!raw) continue
const h = raw.match(/hits?\s+(\d+)/i)
const m = raw.match(/miss(?:es)?\s+(\d+)/i)
if (h) hits = (hits ?? 0) + Number(h[1])
if (m) misses = (misses ?? 0) + Number(m[1])
}
return { hits, misses }
}
export class DmcacheCollector extends CollectorPlugin {
constructor(opts = {}) {
super({ name: 'dmcache', intervalMs: opts.intervalMs })
}
isEnabled() {
return isDmcacheEnabled()
}
start() {
if (!this.isEnabled()) return
registerChart(CHART_STATS)
log.info('dm-cache collector started')
super.start()
}
async collect() {
const stats = await collectDmcacheStats()
if (stats.hits == null && stats.misses == null) return []
const ts = Date.now()
return [
{
chart: 'dmcache.stats',
context: 'dmcache.stats',
ts,
values: { hits: stats.hits, misses: stats.misses },
},
]
}
}
/** @type {DmcacheCollector|null} */
let singleton = null
export function getDmcacheCollector() {
if (!singleton) singleton = new DmcacheCollector()
return singleton
}